diff --git a/errors/categories.go b/errors/categories.go index 451849f1..d125f095 100644 --- a/errors/categories.go +++ b/errors/categories.go @@ -64,4 +64,5 @@ const ( // Recorder RecordingError PlaybackError + PlaybackHashError ) diff --git a/errors/messages.go b/errors/messages.go index 419a58e4..29ebd57a 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -61,6 +61,7 @@ var messages = map[Errno]string{ UnknownPeripheralEvent: "this peripheral (%s) does not understand that event (%v)", // Recorder - RecordingError: "error when recording input (%s)", - PlaybackError: "error when playing back recorded input (%s)", + RecordingError: "error when recording input (%s)", + PlaybackError: "error when playing back recorded input (%s)", + PlaybackHashError: "hash error when playing back recording input (%s)", } diff --git a/gopher2600.go b/gopher2600.go index 7ffe1fa5..3d2a515f 100644 --- a/gopher2600.go +++ b/gopher2600.go @@ -115,7 +115,7 @@ func main() { fallthrough case "PLAY": - tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") + tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") scaling := modeFlags.Float64("scale", 3.0, "television scaling") stable := modeFlags.Bool("stable", true, "wait for stable frame before opening display") record := modeFlags.Bool("record", false, "record user input to a file") @@ -124,12 +124,15 @@ func main() { switch len(modeFlags.Args()) { case 0: - fmt.Println("* 2600 cartridge required") - os.Exit(2) + if *recording == "" { + fmt.Println("* 2600 cartridge required") + os.Exit(2) + } + fallthrough case 1: - err := playmode.Play(modeFlags.Arg(0), *tvMode, float32(*scaling), *stable, *recording, *record) + err := playmode.Play(modeFlags.Arg(0), *tvType, float32(*scaling), *stable, *recording, *record) if err != nil { - fmt.Printf("* error running emulator: %s\n", err) + fmt.Println(err) os.Exit(2) } default: @@ -204,7 +207,7 @@ func main() { case "FPS": display := modeFlags.Bool("display", false, "display TV output: boolean") - tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") + tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") scaling := modeFlags.Float64("scale", 3.0, "television scaling") runTime := modeFlags.String("time", "5s", "run duration (note: there is a 2s overhead)") profile := modeFlags.Bool("profile", false, "perform cpu and memory profiling") @@ -215,7 +218,7 @@ func main() { fmt.Println("* 2600 cartridge required") os.Exit(2) case 1: - err := fps(*profile, modeFlags.Arg(0), *display, *tvMode, float32(*scaling), *runTime) + err := fps(*profile, modeFlags.Arg(0), *display, *tvType, float32(*scaling), *runTime) if err != nil { fmt.Printf("* error starting fps profiler: %s\n", err) os.Exit(2) @@ -275,7 +278,7 @@ func main() { } case "ADD": - tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") + tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") numFrames := modeFlags.Int("frames", 10, "number of frames to run") modeFlagsParse() @@ -284,7 +287,7 @@ func main() { fmt.Println("* 2600 cartridge required") os.Exit(2) case 1: - err := regression.RegressAddCartridge(modeFlags.Arg(0), *tvMode, *numFrames) + err := regression.RegressAddCartridge(modeFlags.Arg(0), *tvType, *numFrames) if err != nil { fmt.Printf("* error adding regression test: %s\n", err) os.Exit(2) @@ -295,7 +298,7 @@ func main() { os.Exit(2) } case "UPDATE": - tvMode := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") + tvType := modeFlags.String("tv", "NTSC", "television specification: NTSC, PAL") numFrames := modeFlags.Int("frames", 10, "number of frames to run") modeFlagsParse() @@ -304,7 +307,7 @@ func main() { fmt.Println("* 2600 cartridge required") os.Exit(2) case 1: - err := regression.RegressUpdateCartridge(modeFlags.Arg(0), *tvMode, *numFrames) + err := regression.RegressUpdateCartridge(modeFlags.Arg(0), *tvType, *numFrames) if err != nil { fmt.Printf("* error updating regression test: %s\n", err) os.Exit(2) @@ -318,12 +321,12 @@ func main() { } } -func fps(profile bool, cartridgeFile string, display bool, tvMode string, scaling float32, runTime string) error { +func fps(profile bool, cartridgeFile string, display bool, tvType string, scaling float32, runTime string) error { var fpstv television.Television var err error if display { - fpstv, err = sdl.NewGUI(tvMode, scaling, nil) + fpstv, err = sdl.NewGUI(tvType, scaling, nil) if err != nil { return fmt.Errorf("error preparing television: %s", err) } diff --git a/hardware/memory/cartridge.go b/hardware/memory/cartridge.go index 536f819a..3a8f5f97 100644 --- a/hardware/memory/cartridge.go +++ b/hardware/memory/cartridge.go @@ -1,6 +1,7 @@ package memory import ( + "crypto/sha1" "fmt" "gopher2600/errors" "io" @@ -20,7 +21,14 @@ type Cartridge struct { Area AreaInfo - name string + // full path to the cartridge as stored on disk + Filename string + + // hash of binary loaded from disk. any subsequent pokes to cartridge + // memory will not be reflected in the value + Hash string + + // cartridge bank-switching method method string NumBanks int @@ -40,6 +48,7 @@ type Cartridge struct { } const ejectedName = "ejected" +const ejectedHash = "nohash" const ejectedMethod = "none" // NewCart is the preferred method of initialisation for the cartridges @@ -56,12 +65,12 @@ func NewCart() *Cartridge { // MachineInfoTerse returns the cartridge information in terse format func (cart Cartridge) MachineInfoTerse() string { - return fmt.Sprintf("%s [%s] bank=%d", cart.name, cart.method, cart.Bank) + return fmt.Sprintf("%s [%s] bank=%d", cart.Filename, cart.method, cart.Bank) } // MachineInfo returns the cartridge information in verbose format func (cart Cartridge) MachineInfo() string { - return fmt.Sprintf("name: %s\nmethod: %s\nbank:%d", cart.name, cart.method, cart.Bank) + return fmt.Sprintf("name: %s\nmethod: %s\nbank:%d", cart.Filename, cart.method, cart.Bank) } // Label is an implementation of Area.Label @@ -146,6 +155,14 @@ func (cart *Cartridge) Attach(filename string) error { return err } + // generate hash + key := sha1.New() + if _, err := io.Copy(key, cf); err != nil { + return err + } + cart.Hash = fmt.Sprintf("%x", key.Sum(nil)) + + // we always start in bank 0 cart.Bank = 0 // set default read hooks @@ -354,7 +371,7 @@ func (cart *Cartridge) Attach(filename string) error { } // note name of cartridge - cart.name = filename + cart.Filename = filename return nil } @@ -418,8 +435,9 @@ func (cart *Cartridge) BankSwitch(bank int) error { // Eject removes memory from cartridge space and unlike the real hardware, // attaches a bank of empty memory - for convenience of the debugger func (cart *Cartridge) Eject() { - cart.name = ejectedName + cart.Filename = ejectedName cart.method = ejectedMethod + cart.Hash = ejectedHash cart.NumBanks = 1 cart.Bank = 0 cart.readBanks(nil, cart.NumBanks) diff --git a/playmode/play.go b/playmode/play.go index 6f9474fe..cebe5543 100644 --- a/playmode/play.go +++ b/playmode/play.go @@ -9,13 +9,12 @@ import ( "gopher2600/recorder" "path" "strings" - "sync/atomic" "time" ) // Play sets the emulation running - without any debugging features -func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording string, newRecording bool) error { - playtv, err := sdl.NewGUI(tvMode, scaling, nil) +func Play(cartridgeFile, tvType string, scaling float32, stable bool, recording string, newRecording bool) error { + playtv, err := sdl.NewGUI(tvType, scaling, nil) if err != nil { return fmt.Errorf("error preparing television: %s", err) } @@ -31,11 +30,6 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording } vcs.Ports.Player0.Attach(stk) - err = vcs.AttachCartridge(cartridgeFile) - if err != nil { - return err - } - // create default recording file name if no name has been supplied if newRecording && recording == "" { shortCartName := path.Base(cartridgeFile) @@ -45,35 +39,67 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording recording = fmt.Sprintf("recording_%s_%s", shortCartName, timestamp) } + var rec *recorder.Recorder + var plb *recorder.Playback + + // note that we attach the cartridge in three different branches below - we + // need to do this at different times depending on whether a new recording + // or playback is taking place; or if it's just a regular playback + if recording != "" { if newRecording { - recording, err := recorder.NewRecorder(recording, vcs) + err = vcs.AttachCartridge(cartridgeFile) if err != nil { - return fmt.Errorf("error preparing VCS: %s", err) + return err + } + + rec, err = recorder.NewRecorder(recording, vcs) + if err != nil { + return fmt.Errorf("error preparing recording: %s", err) } defer func() { - recording.End() + rec.End() }() - vcs.Ports.Player0.AttachTranscriber(recording) - vcs.Ports.Player1.AttachTranscriber(recording) - vcs.Panel.AttachTranscriber(recording) + vcs.Ports.Player0.AttachTranscriber(rec) + vcs.Ports.Player1.AttachTranscriber(rec) + vcs.Panel.AttachTranscriber(rec) } else { - recording, err := recorder.NewPlayback(recording, vcs) + plb, err = recorder.NewPlayback(recording, vcs) if err != nil { - return fmt.Errorf("error preparing VCS: %s", err) + return fmt.Errorf("error playing back recording: %s", err) } - vcs.Ports.Player0.Attach(recording) - vcs.Ports.Player1.Attach(recording) - vcs.Panel.Attach(recording) + vcs.Ports.Player0.Attach(plb) + vcs.Ports.Player1.Attach(plb) + vcs.Panel.Attach(plb) + + if cartridgeFile != "" && cartridgeFile != plb.CartName { + return fmt.Errorf("error playing back recording: cartridge name doesn't match the name in the recording") + } + + // if no cartridge filename has been provided then use the one in + // the playback file + cartridgeFile = plb.CartName + + err = vcs.AttachCartridge(cartridgeFile) + if err != nil { + return err + } + } + } else { + err = vcs.AttachCartridge(cartridgeFile) + if err != nil { + return err } } - // run while value of running variable is positive - var running atomic.Value - running.Store(0) + // now that we've attached the cartridge check the hash against the + // playback has (if it exists) + if plb != nil && plb.CartHash != vcs.Mem.Cart.Hash { + return fmt.Errorf("error playing back recording: cartridge hash doesn't match") + } // connect debugger to gui guiChannel := make(chan gui.Event, 2) @@ -90,6 +116,7 @@ func Play(cartridgeFile, tvMode string, scaling float32, stable bool, recording } + // run and handle gui events return vcs.Run(func() (bool, error) { select { case ev := <-guiChannel: diff --git a/recorder/fileformat.go b/recorder/fileformat.go new file mode 100644 index 00000000..440e8fe5 --- /dev/null +++ b/recorder/fileformat.go @@ -0,0 +1,74 @@ +package recorder + +import ( + "fmt" + "gopher2600/errors" + "io" + "strings" +) + +const ( + fieldID int = iota + fieldEvent + fieldFrame + fieldScanline + fieldHorizPos + fieldHash + numFields +) + +const fieldSep = ", " + +// playback file header format +// --------------------------- +// +// # +// # +// # + +const ( + lineCartName int = iota + lineCartHash + lineTVtype + numHeaderLines +) + +func (rec *Recorder) writeHeader() error { + lines := make([]string, numHeaderLines) + + // add header information + lines[lineCartName] = rec.vcs.Mem.Cart.Filename + lines[lineCartHash] = rec.vcs.Mem.Cart.Hash + lines[lineTVtype] = fmt.Sprintf("%v\n", rec.vcs.TV.GetSpec().ID) + + line := strings.Join(lines, "\n") + + n, err := io.WriteString(rec.output, line) + + if err != nil { + rec.output.Close() + return errors.NewFormattedError(errors.RecordingError, err) + } + + if n != len(line) { + rec.output.Close() + return errors.NewFormattedError(errors.RecordingError, "output truncated") + } + + return nil +} + +func (plb *Playback) readHeader(lines []string) error { + // read header + plb.CartName = lines[lineCartName] + plb.CartHash = lines[lineCartHash] + plb.TVtype = lines[lineTVtype] + + // validate header + tvspec := plb.vcs.TV.GetSpec() + if tvspec.ID != lines[lineTVtype] { + return errors.NewFormattedError(errors.PlaybackError, "current TV type does not match that in the recording") + } + + return nil +} diff --git a/recorder/playback.go b/recorder/playback.go index b4dadb15..998a5019 100644 --- a/recorder/playback.go +++ b/recorder/playback.go @@ -6,6 +6,7 @@ import ( "gopher2600/hardware" "gopher2600/hardware/peripherals" "gopher2600/television" + "gopher2600/television/renderers" "io/ioutil" "os" "strconv" @@ -17,6 +18,10 @@ type event struct { frame int scanline int horizpos int + hash string + + // the line in the recording file the playback event appears + line int } type playbackSequence struct { @@ -27,12 +32,19 @@ type playbackSequence struct { // Playback is an implementation of the controller interface. it reads from an // existing recording file and responds to GetInput() requests type Playback struct { + CartName string + CartHash string + TVtype string + vcs *hardware.VCS + digest *renderers.DigestTV sequences map[string]*playbackSequence } // NewPlayback is hte preferred method of implementation for the Playback type func NewPlayback(transcript string, vcs *hardware.VCS) (*Playback, error) { + var err error + // check we're working with correct information if vcs == nil || vcs.TV == nil { return nil, errors.NewFormattedError(errors.PlaybackError, "no playback hardware available") @@ -41,73 +53,83 @@ func NewPlayback(transcript string, vcs *hardware.VCS) (*Playback, error) { plb := &Playback{vcs: vcs} plb.sequences = make(map[string]*playbackSequence) - // open file + // create digesttv, piggybacking on the tv already being used by vcs + plb.digest, err = renderers.NewDigestTV(vcs.TV.GetSpec().ID, vcs.TV) + if err != nil { + return nil, errors.NewFormattedError(errors.RecordingError, err) + } + + // open file; read the entirity of the contents; close file tf, err := os.Open(transcript) if err != nil { return nil, errors.NewFormattedError(errors.PlaybackError, err) } - buffer, err := ioutil.ReadAll(tf) if err != nil { return nil, errors.NewFormattedError(errors.PlaybackError, err) } + err = tf.Close() + if err != nil { + return nil, errors.NewFormattedError(errors.PlaybackError, err) + } - _ = tf.Close() - - // convert buffer to an array of lines + // convert file contents to an array of lines lines := strings.Split(string(buffer), "\n") - // read header - tvspec := plb.vcs.TV.GetSpec() - if tvspec.ID != lines[0] { - return nil, errors.NewFormattedError(errors.PlaybackError, "current TV type does not match that in the recording") + // read header and perform validation checks + err = plb.readHeader(lines) + if err != nil { + return nil, err } // loop through transcript and divide events according to the first field // (the ID) - for i := 1; i < len(lines)-1; i++ { + for i := numHeaderLines; i < len(lines)-1; i++ { toks := strings.Split(lines[i], fieldSep) // ignore lines that don't have enough fields if len(toks) != numFields { - continue + msg := fmt.Sprintf("expected %d fields at line %d", numFields, i+1) + return nil, errors.NewFormattedError(errors.PlaybackError, msg) } // add a new playbackSequence for the id if it doesn't exist - id := toks[0] + id := toks[fieldID] if _, ok := plb.sequences[id]; !ok { plb.sequences[id] = &playbackSequence{} } // create a new event and convert tokens accordingly // any errors in the transcript causes failure - event := event{} + event := event{line: i + 1} - n, err := strconv.Atoi(toks[1]) + n, err := strconv.Atoi(toks[fieldEvent]) if err != nil { - msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:2], fieldSep))) + msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:2], fieldSep))) return nil, errors.NewFormattedError(errors.PlaybackError, msg) } event.event = peripherals.Event(n) - event.frame, err = strconv.Atoi(toks[2]) + event.frame, err = strconv.Atoi(toks[fieldFrame]) if err != nil { - msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:3], fieldSep))) + msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:3], fieldSep))) return nil, errors.NewFormattedError(errors.PlaybackError, msg) } - event.scanline, err = strconv.Atoi(toks[3]) + event.scanline, err = strconv.Atoi(toks[fieldScanline]) if err != nil { - msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:4], fieldSep))) + msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:4], fieldSep))) return nil, errors.NewFormattedError(errors.PlaybackError, msg) } - event.horizpos, err = strconv.Atoi(toks[4]) + event.horizpos, err = strconv.Atoi(toks[fieldHorizPos]) if err != nil { - msg := fmt.Sprintf("line %d, col %d", i+1, len(strings.Join(toks[:5], fieldSep))) + msg := fmt.Sprintf("%s line %d, col %d", err, i+1, len(strings.Join(toks[:5], fieldSep))) return nil, errors.NewFormattedError(errors.PlaybackError, msg) } + event.hash = toks[fieldHash] + // add new event to list of events in the correct playback sequence seq := plb.sequences[id] seq.events = append(seq.events, event) @@ -146,6 +168,11 @@ func (plb *Playback) GetInput(id string) (peripherals.Event, error) { // compare current state with the state in the transcript nextEvent := seq.events[seq.eventCt] if frame == nextEvent.frame && scanline == nextEvent.scanline && horizpos == nextEvent.horizpos { + if nextEvent.hash != plb.digest.String() { + msg := fmt.Sprintf("line %d", nextEvent.line) + return peripherals.NoEvent, errors.NewFormattedError(errors.PlaybackHashError, msg) + } + seq.eventCt++ return nextEvent.event, nil } diff --git a/recorder/record.go b/recorder/record.go index 32acdf1f..736417be 100644 --- a/recorder/record.go +++ b/recorder/record.go @@ -6,32 +6,39 @@ import ( "gopher2600/hardware" "gopher2600/hardware/peripherals" "gopher2600/television" + "gopher2600/television/renderers" "io" "os" ) -const fieldSep = ", " -const numFields = 5 - // Recorder records controller events to disk, intended for future playback type Recorder struct { vcs *hardware.VCS + digest *renderers.DigestTV output *os.File } // NewRecorder is the preferred method of implementation for the FileRecorder type func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) { + var err error + // check we're working with correct information if vcs == nil || vcs.TV == nil { return nil, errors.NewFormattedError(errors.RecordingError, "hardware is not suitable for recording") } - scr := &Recorder{vcs: vcs} + rec := &Recorder{vcs: vcs} + + // create digesttv, piggybacking on the tv already being used by vcs + rec.digest, err = renderers.NewDigestTV(vcs.TV.GetSpec().ID, vcs.TV) + if err != nil { + return nil, errors.NewFormattedError(errors.RecordingError, err) + } // open file - _, err := os.Stat(transcript) + _, err = os.Stat(transcript) if os.IsNotExist(err) { - scr.output, err = os.Create(transcript) + rec.output, err = os.Create(transcript) if err != nil { return nil, errors.NewFormattedError(errors.RecordingError, "can't create file") } @@ -39,26 +46,17 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) { return nil, errors.NewFormattedError(errors.RecordingError, "file already exists") } - // add header information - tvspec := scr.vcs.TV.GetSpec() - line := fmt.Sprintf("%v\n", tvspec.ID) - - n, err := io.WriteString(scr.output, line) + err = rec.writeHeader() if err != nil { - scr.output.Close() - return nil, errors.NewFormattedError(errors.RecordingError, err) - } - if n != len(line) { - scr.output.Close() - return nil, errors.NewFormattedError(errors.RecordingError, "output truncated") + return nil, err } - return scr, nil + return rec, nil } // End closes the output file. -func (scr *Recorder) End() error { - err := scr.output.Close() +func (rec *Recorder) End() error { + err := rec.output.Close() if err != nil { return errors.NewFormattedError(errors.RecordingError, err) } @@ -67,38 +65,49 @@ func (scr *Recorder) End() error { } // Transcribe implements the Transcriber interface -func (scr *Recorder) Transcribe(id string, event peripherals.Event) error { +func (rec *Recorder) Transcribe(id string, event peripherals.Event) error { // don't do anything if event is the NoEvent if event == peripherals.NoEvent { return nil } // sanity checks - if scr.output == nil { + if rec.output == nil { return errors.NewFormattedError(errors.RecordingError, "recording file is not open") } - if scr.vcs == nil || scr.vcs.TV == nil { + if rec.vcs == nil || rec.vcs.TV == nil { return errors.NewFormattedError(errors.RecordingError, "hardware is not suitable for recording") } // create line and write to file - frame, err := scr.vcs.TV.GetState(television.ReqFramenum) + frame, err := rec.vcs.TV.GetState(television.ReqFramenum) if err != nil { return err } - scanline, err := scr.vcs.TV.GetState(television.ReqScanline) + scanline, err := rec.vcs.TV.GetState(television.ReqScanline) if err != nil { return err } - horizpos, err := scr.vcs.TV.GetState(television.ReqHorizPos) + horizpos, err := rec.vcs.TV.GetState(television.ReqHorizPos) if err != nil { return err } - line := fmt.Sprintf("%v%s%v%s%v%s%v%s%v\n", id, fieldSep, event, fieldSep, frame, fieldSep, scanline, fieldSep, horizpos) + line := fmt.Sprintf("%v%s%v%s%v%s%v%s%v%s%v\n", id, + fieldSep, + event, + fieldSep, + frame, + fieldSep, + scanline, + fieldSep, + horizpos, + fieldSep, + rec.digest.String(), + ) - n, err := io.WriteString(scr.output, line) + n, err := io.WriteString(rec.output, line) if err != nil { return errors.NewFormattedError(errors.RecordingError, err) } diff --git a/regression/database.go b/regression/database.go index 65772e2f..21fd468e 100644 --- a/regression/database.go +++ b/regression/database.go @@ -1,7 +1,6 @@ package regression import ( - "crypto/sha1" "encoding/csv" "fmt" "gopher2600/errors" @@ -12,29 +11,15 @@ import ( const regressionDBFile = ".gopher2600/regressionDB" -func getCartridgeHash(cartridgeFile string) (string, error) { - f, err := os.Open(cartridgeFile) - if err != nil { - return "", err - } - defer f.Close() - - key := sha1.New() - if _, err := io.Copy(key, f); err != nil { - return "", err - } - - return fmt.Sprintf("%x", key.Sum(nil)), nil -} - type regressionEntry struct { - cartridgeHash string cartridgePath string tvMode string numOFrames int screenDigest string } +const numFields = 4 + func (entry regressionEntry) String() string { return fmt.Sprintf("%s [%s] frames=%d", entry.cartridgePath, entry.tvMode, entry.numOFrames) } @@ -62,36 +47,37 @@ func startSession() (*regressionDB, error) { return db, nil } -func (db *regressionDB) endSession() error { +func (db *regressionDB) endSession(commitChanges bool) error { // write entries to regression database - csvw := csv.NewWriter(db.dbfile) + if commitChanges { + csvw := csv.NewWriter(db.dbfile) - err := db.dbfile.Truncate(0) - if err != nil { - return err - } - - db.dbfile.Seek(0, os.SEEK_SET) - - for _, entry := range db.entries { - rec := make([]string, 5) - rec[0] = entry.cartridgeHash - rec[1] = entry.cartridgePath - rec[2] = entry.tvMode - rec[3] = strconv.Itoa(entry.numOFrames) - rec[4] = entry.screenDigest - - err := csvw.Write(rec) + err := db.dbfile.Truncate(0) if err != nil { return err } - } - // make sure everything's been written - csvw.Flush() - err = csvw.Error() - if err != nil { - return err + db.dbfile.Seek(0, os.SEEK_SET) + + for _, entry := range db.entries { + rec := make([]string, numFields) + rec[0] = entry.cartridgePath + rec[1] = entry.tvMode + rec[2] = strconv.Itoa(entry.numOFrames) + rec[3] = entry.screenDigest + + err := csvw.Write(rec) + if err != nil { + return err + } + } + + // make sure everything's been written + csvw.Flush() + err = csvw.Error() + if err != nil { + return err + } } // end session by closing file @@ -114,7 +100,7 @@ func (db *regressionDB) readEntries() error { csvr.Comment = rune('#') csvr.TrimLeadingSpace = true csvr.ReuseRecord = true - csvr.FieldsPerRecord = 5 + csvr.FieldsPerRecord = numFields db.dbfile.Seek(0, os.SEEK_SET) @@ -128,20 +114,19 @@ func (db *regressionDB) readEntries() error { return err } - numOfFrames, err := strconv.Atoi(rec[3]) + numOfFrames, err := strconv.Atoi(rec[2]) if err != nil { return err } // add entry to database entry := regressionEntry{ - cartridgeHash: rec[0], - cartridgePath: rec[1], - tvMode: rec[2], + cartridgePath: rec[0], + tvMode: rec[1], numOFrames: numOfFrames, - screenDigest: rec[4]} + screenDigest: rec[3]} - db.entries[entry.cartridgeHash] = entry + db.entries[entry.cartridgePath] = entry } return nil @@ -153,7 +138,7 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd if err != nil { return err } - defer db.endSession() + defer db.endSession(true) // run cartdrige and get digest digest, err := run(cartridgeFile, tvMode, numOfFrames) @@ -161,20 +146,14 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd return err } - // add new entry to database - key, err := getCartridgeHash(cartridgeFile) - if err != nil { - return err - } entry := regressionEntry{ - cartridgeHash: key, cartridgePath: cartridgeFile, tvMode: tvMode, numOFrames: numOfFrames, screenDigest: digest} if allowUpdate == false { - if existEntry, ok := db.entries[entry.cartridgeHash]; ok { + if existEntry, ok := db.entries[entry.cartridgePath]; ok { if existEntry.cartridgePath == entry.cartridgePath { return errors.NewFormattedError(errors.RegressionEntryExists, entry) } @@ -183,7 +162,7 @@ func addCartridge(cartridgeFile string, tvMode string, numOfFrames int, allowUpd } } - db.entries[entry.cartridgeHash] = entry + db.entries[entry.cartridgePath] = entry return nil } diff --git a/regression/regression.go b/regression/regression.go index 711820dd..9bac3805 100644 --- a/regression/regression.go +++ b/regression/regression.go @@ -14,18 +14,13 @@ func RegressDeleteCartridge(cartridgeFile string) error { if err != nil { return err } - defer db.endSession() + defer db.endSession(true) - key, err := getCartridgeHash(cartridgeFile) - if err != nil { - return err - } - - if _, ok := db.entries[key]; ok == false { + if _, ok := db.entries[cartridgeFile]; ok == false { return errors.NewFormattedError(errors.RegressionEntryDoesNotExist, cartridgeFile) } - delete(db.entries, key) + delete(db.entries, cartridgeFile) return nil } @@ -46,7 +41,7 @@ func RegressRunTests(output io.Writer, failOnError bool) (int, int, error) { if err != nil { return -1, -1, err } - defer db.endSession() + defer db.endSession(false) numSucceed := 0 numFail := 0