mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 05:40:49 -04:00
24f3f32342
notifications interface instance moved to environment from cartridgeloader. the cartridgeloader package predates the environment package and had started to be used inappropriately simplified how notifications.Notify() is called. in particular the supercharger fastload starter no longer bundles a function hook. nor is the cartridge instance sent with the notification
239 lines
6.6 KiB
Go
239 lines
6.6 KiB
Go
// This file is part of Gopher2600.
|
|
//
|
|
// Gopher2600 is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Gopher2600 is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package regression
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jetsetilly/gopher2600/database"
|
|
"github.com/jetsetilly/gopher2600/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/digest"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
|
"github.com/jetsetilly/gopher2600/hardware/television"
|
|
"github.com/jetsetilly/gopher2600/recorder"
|
|
)
|
|
|
|
const playbackEntryType = "playback"
|
|
|
|
const (
|
|
playbackFieldScript int = iota
|
|
playbackFieldNotes
|
|
numPlaybackFields
|
|
)
|
|
|
|
// PlaybackRegression represents a regression type that processes a VCS
|
|
// recording. playback regressions can take a while to run because by their
|
|
// nature they extend over many frames - many more than is typical with the
|
|
// FrameRegression type.
|
|
type PlaybackRegression struct {
|
|
Script string
|
|
Notes string
|
|
}
|
|
|
|
func deserialisePlaybackEntry(fields database.SerialisedEntry) (database.Entry, error) {
|
|
reg := &PlaybackRegression{}
|
|
|
|
// basic sanity check
|
|
if len(fields) > numPlaybackFields {
|
|
return nil, fmt.Errorf("playback: too many fields")
|
|
}
|
|
if len(fields) < numPlaybackFields {
|
|
return nil, fmt.Errorf("playback: too few fields")
|
|
}
|
|
|
|
// string fields need no conversion
|
|
reg.Script = fields[playbackFieldScript]
|
|
reg.Notes = fields[playbackFieldNotes]
|
|
|
|
return reg, nil
|
|
}
|
|
|
|
// EntryType implements the database.Entry interface.
|
|
func (reg PlaybackRegression) EntryType() string {
|
|
return playbackEntryType
|
|
}
|
|
|
|
// Serialise implements the database.Entry interface.
|
|
func (reg *PlaybackRegression) Serialise() (database.SerialisedEntry, error) {
|
|
return database.SerialisedEntry{
|
|
reg.Script,
|
|
reg.Notes,
|
|
},
|
|
nil
|
|
}
|
|
|
|
// CleanUp implements the database.Entry interface.
|
|
func (reg PlaybackRegression) CleanUp() error {
|
|
err := os.Remove(reg.Script)
|
|
if _, ok := err.(*os.PathError); ok {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// String implements the regression.Regressor interface.
|
|
func (reg PlaybackRegression) String() string {
|
|
s := strings.Builder{}
|
|
s.WriteString(fmt.Sprintf("[%s] %s", reg.EntryType(), filepath.Base(reg.Script)))
|
|
if reg.Notes != "" {
|
|
s.WriteString(fmt.Sprintf(" [%s]", reg.Notes))
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// regress implements the regression.Regressor interface.
|
|
func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg string) (_ bool, _ string, rerr error) {
|
|
output.Write([]byte(msg))
|
|
|
|
plb, err := recorder.NewPlayback(reg.Script, true)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
tv, err := television.NewTelevision(plb.TVSpec)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
defer tv.End()
|
|
tv.SetFPSCap(false)
|
|
|
|
_, err = digest.NewVideo(tv)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
vcs, err := hardware.NewVCS(tv, nil, nil)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// for playback regression to work correctly we want the VCS to be a known
|
|
// starting state. this will be handled in the playback.AttachToVCS
|
|
// function according to the current features of the recorder package and
|
|
// the saved script
|
|
|
|
err = plb.AttachToVCSInput(vcs)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// not using setup.AttachCartridge. if the playback was recorded with setup
|
|
// changes the events will have been copied into the playback script and
|
|
// will be applied that way
|
|
err = vcs.AttachCartridge(plb.CartLoad, true)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// prepare ticker for progress meter
|
|
dur, _ := time.ParseDuration("1s")
|
|
tck := time.NewTicker(dur)
|
|
|
|
// run emulation
|
|
err = vcs.Run(func() (govern.State, error) {
|
|
// if the CPU is in the KIL state then the test will never end normally
|
|
if vcs.CPU.Killed {
|
|
return govern.Ending, fmt.Errorf("CPU in KIL state")
|
|
}
|
|
|
|
hasEnded, err := plb.EndFrame()
|
|
if err != nil {
|
|
return govern.Ending, fmt.Errorf("playback: %w", err)
|
|
}
|
|
if hasEnded {
|
|
return govern.Ending, fmt.Errorf("playback: ended unexpectedly")
|
|
}
|
|
|
|
// display progress meter every 1 second
|
|
select {
|
|
case <-tck.C:
|
|
output.Write([]byte(fmt.Sprintf("\r%s [%s]", msg, plb)))
|
|
default:
|
|
}
|
|
return govern.Running, nil
|
|
})
|
|
|
|
if err != nil {
|
|
if errors.Is(err, ports.PowerOff) {
|
|
// PowerOff is okay and is to be expected
|
|
} else if errors.Is(err, recorder.PlaybackHashError) {
|
|
// PlaybackHashError means that a screen digest somewhere in the
|
|
// playback script did not work. filter error and return false to
|
|
// indicate failure
|
|
coords := tv.GetCoords()
|
|
return false, fmt.Sprintf("%v: at fr=%d, sl=%d, cl=%d", err, coords.Frame, coords.Scanline, coords.Clock), nil
|
|
} else {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
}
|
|
|
|
// if this is a new regression we want to store the script in the
|
|
// regressionScripts directory
|
|
if newRegression {
|
|
// create a unique filename
|
|
newScript, err := uniqueFilename("playback", plb.CartLoad.ShortName())
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// check that the filename is unique
|
|
nf, _ := os.Open(newScript)
|
|
// no need to bother with returned error. nf tells us everything we
|
|
// need
|
|
if nf != nil {
|
|
return false, "", fmt.Errorf("playback: script already exists (%s)", newScript)
|
|
}
|
|
nf.Close()
|
|
|
|
// create new file
|
|
nf, err = os.Create(newScript)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: while copying playback script: %w", err)
|
|
}
|
|
defer func() {
|
|
err := nf.Close()
|
|
if err != nil {
|
|
rerr = fmt.Errorf("playback: while copying playback script: %w", err)
|
|
}
|
|
}()
|
|
|
|
// open old file
|
|
of, err := os.Open(reg.Script)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: while copying playback script: %w", err)
|
|
}
|
|
defer of.Close()
|
|
|
|
// copy old file to new file
|
|
_, err = io.Copy(nf, of)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("playback: while copying playback script: %w", err)
|
|
}
|
|
|
|
// update script name in regression type
|
|
reg.Script = newScript
|
|
}
|
|
|
|
return true, "", nil
|
|
}
|