package tracker_test import ( "bytes" "encoding/binary" "fmt" "io" "testing" "github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/vm" ) type NullContext struct{} func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) { return tracker.MIDINoteEvent{}, false } func (NullContext) BPM() (bpm float64, ok bool) { return 0, false } func (NullContext) InputDevices(yield func(tracker.MIDIDevice) bool) {} func (NullContext) Close() {} type modelFuzzState struct { model *tracker.Model clipboard []byte file []byte } type myWriteCloser struct { *bytes.Buffer } func (mwc *myWriteCloser) Close() error { // Noop return nil } func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)) bool, seed int) { // Ints s.IterateInt("InstrumentVoices", s.model.InstrumentVoices().Int(), yield, seed) s.IterateInt("TrackVoices", s.model.TrackVoices().Int(), yield, seed) s.IterateInt("SongLength", s.model.SongLength().Int(), yield, seed) s.IterateInt("BPM", s.model.BPM().Int(), yield, seed) s.IterateInt("RowsPerPattern", s.model.RowsPerPattern().Int(), yield, seed) s.IterateInt("RowsPerBeat", s.model.RowsPerBeat().Int(), yield, seed) s.IterateInt("Step", s.model.Step().Int(), yield, seed) s.IterateInt("Octave", s.model.Octave().Int(), yield, seed) // Lists s.IterateList("Instruments", s.model.Instruments().List(), yield, seed) s.IterateList("Units", s.model.Units().List(), yield, seed) s.IterateList("Tracks", s.model.Tracks().List(), yield, seed) s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed) s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed) s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed) s.IterateBool("Panic", s.model.Panic().Bool(), yield, seed) s.IterateBool("Recording", s.model.IsRecording().Bool(), yield, seed) s.IterateBool("Playing", s.model.Playing().Bool(), yield, seed) s.IterateBool("InstrEnlarged", s.model.InstrEnlarged().Bool(), yield, seed) s.IterateBool("Effect", s.model.Effect().Bool(), yield, seed) s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed) s.IterateBool("Follow", s.model.Follow().Bool(), yield, seed) s.IterateBool("UniquePatterns", s.model.UniquePatterns().Bool(), yield, seed) s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack().Bool(), yield, seed) // Strings s.IterateString("FilePath", s.model.FilePath().String(), yield, seed) s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed) s.IterateString("InstrumentComment", s.model.InstrumentComment().String(), yield, seed) s.IterateString("UnitSearchText", s.model.UnitSearch().String(), yield, seed) // Actions s.IterateAction("AddTrack", s.model.AddTrack(), yield, seed) s.IterateAction("DeleteTrack", s.model.DeleteTrack(), yield, seed) s.IterateAction("AddInstrument", s.model.AddInstrument(), yield, seed) s.IterateAction("DeleteInstrument", s.model.DeleteInstrument(), yield, seed) s.IterateAction("AddUnitAfter", s.model.AddUnit(false), yield, seed) s.IterateAction("AddUnitBefore", s.model.AddUnit(true), yield, seed) s.IterateAction("DeleteUnit", s.model.DeleteUnit(), yield, seed) s.IterateAction("ClearUnit", s.model.ClearUnit(), yield, seed) s.IterateAction("Undo", s.model.Undo(), yield, seed) s.IterateAction("Redo", s.model.Redo(), yield, seed) s.IterateAction("RemoveUnused", s.model.RemoveUnused(), yield, seed) s.IterateAction("AddSemitone", s.model.AddSemitone(), yield, seed) s.IterateAction("SubtractSemitone", s.model.SubtractSemitone(), yield, seed) s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed) s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed) s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed) s.IterateAction("PlaySongStart", s.model.PlaySongStart(), yield, seed) s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed) s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed) s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed) s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed) s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed) s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed) // just test loading one of the presets s.IterateAction("LoadPreset", s.model.LoadPreset(seed%tracker.NumPresets()), yield, seed) // Tables s.IterateTable("Order", s.model.Order().Table(), yield, seed) s.IterateTable("Notes", s.model.Notes().Table(), yield, seed) // File reading if s.file != nil { yield("ReadSong", func(p string, t *testing.T) { reader := bytes.NewReader(s.file) readCloser := io.NopCloser(reader) s.model.ReadSong(readCloser) }) yield("LoadInstrument", func(p string, t *testing.T) { reader := bytes.NewReader(s.file) readCloser := io.NopCloser(reader) s.model.LoadInstrument(readCloser) }) } // File saving yield("WriteSong", func(p string, t *testing.T) { writer := bytes.NewBuffer(nil) writeCloser := &myWriteCloser{writer} s.model.WriteSong(writeCloser) s.file = writer.Bytes() }) yield("SaveInstrument", func(p string, t *testing.T) { writer := bytes.NewBuffer(nil) writeCloser := &myWriteCloser{writer} s.model.SaveInstrument(writeCloser) s.file = writer.Bytes() }) } func (s *modelFuzzState) IterateInt(name string, i tracker.Int, yield func(string, func(p string, t *testing.T)) bool, seed int) { r := i.Range() yield(name+".Set", func(p string, t *testing.T) { i.Set(seed%(r.Max-r.Min+10) - 5 + r.Min) }) yield(name+".Value", func(p string, t *testing.T) { if v := i.Value(); v < r.Min || v > r.Max { r := i.Range() t.Errorf("Path: %s %s value out of range [%d,%d]: %d", p, name, r.Min, r.Max, v) } }) } func (s *modelFuzzState) IterateAction(name string, a tracker.Action, yield func(string, func(p string, t *testing.T)) bool, seed int) { yield(name+".Do", func(p string, t *testing.T) { a.Do() }) } func (s *modelFuzzState) IterateBool(name string, b tracker.Bool, yield func(string, func(p string, t *testing.T)) bool, seed int) { yield(name+".Set", func(p string, t *testing.T) { b.Set(seed%2 == 0) }) yield(name+".Toggle", func(p string, t *testing.T) { b.Toggle() }) } func (s *modelFuzzState) IterateString(name string, str tracker.String, yield func(string, func(p string, t *testing.T)) bool, seed int) { yield(name+".Set", func(p string, t *testing.T) { str.Set(fmt.Sprintf("%d", seed)) }) } func (s *modelFuzzState) IterateList(name string, l tracker.List, yield func(string, func(p string, t *testing.T)) bool, seed int) { yield(name+".SetSelected", func(p string, t *testing.T) { l.SetSelected(seed%50 - 16) }) yield(name+".Count", func(p string, t *testing.T) { if c := l.Count(); c > 0 { if l.Selected() < 0 || l.Selected() >= c { t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected()) } } else { if l.Selected() != 0 { t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected()) } } }) yield(name+".SetSelected2", func(p string, t *testing.T) { l.SetSelected2(seed%50 - 16) }) yield(name+".Count2", func(p string, t *testing.T) { if c := l.Count(); c > 0 { if l.Selected2() < 0 || l.Selected2() >= c { t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2()) } } else { if l.Selected2() != 0 { t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2()) } } }) yield(name+".MoveElements", func(p string, t *testing.T) { l.MoveElements(seed%2*2 - 1) }) yield(name+".DeleteElementsForward", func(p string, t *testing.T) { l.DeleteElements(false) }) yield(name+".DeleteElementsBackward", func(p string, t *testing.T) { l.DeleteElements(true) }) yield(name+".CopyElements", func(p string, t *testing.T) { s.clipboard, _ = l.CopyElements() }) yield(name+".PasteElements", func(p string, t *testing.T) { l.PasteElements(s.clipboard) }) } func (s *modelFuzzState) IterateTable(name string, table tracker.Table, yield func(string, func(p string, t *testing.T)) bool, seed int) { yield(name+".SetCursor", func(p string, t *testing.T) { table.SetCursor(tracker.Point{seed % 16, seed * 1337 % 16}) }) yield(name+".SetCursor2", func(p string, t *testing.T) { table.SetCursor2(tracker.Point{seed % 16, seed * 1337 % 16}) }) yield(name+".Cursor", func(p string, t *testing.T) { if c := table.Cursor(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) { t.Errorf("Path: %s Table cursor out of range: %v", p, c) } }) yield(name+".Cursor2", func(p string, t *testing.T) { if c := table.Cursor2(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) { t.Errorf("Path: %s Table cursor2 out of range: %v", p, c) } }) yield(name+".SetCursorX", func(p string, t *testing.T) { table.SetCursorX(seed % 16) }) yield(name+".SetCursorY", func(p string, t *testing.T) { table.SetCursorY(seed % 16) }) yield(name+".MoveCursor", func(p string, t *testing.T) { table.MoveCursor(seed%2*2-1, seed%2*2-1) }) yield(name+".Copy", func(p string, t *testing.T) { s.clipboard, _ = table.Copy() }) yield(name+".Paste", func(p string, t *testing.T) { table.Paste(s.clipboard) }) yield(name+".Clear", func(p string, t *testing.T) { table.Clear() }) yield(name+".Fill", func(p string, t *testing.T) { table.Fill(seed % 16) }) yield(name+".Add", func(p string, t *testing.T) { table.Add(seed % 16) }) } func FuzzModel(f *testing.F) { seed := make([]byte, 1) for i := range seed { seed[i] = byte(i) } f.Add(seed) f.Fuzz(func(t *testing.T, slice []byte) { reader := bytes.NewReader(slice) synther := vm.GoSynther{} model, player := tracker.NewModelPlayer(synther, NullContext{}, "") buf := make([][2]float32, 2048) closeChan := make(chan struct{}) go func() { loop: for { select { case <-closeChan: break loop default: ctx := NullContext{} player.Process(buf, ctx, nil) } } }() state := modelFuzzState{model: model} count := 0 state.Iterate(func(n string, f func(p string, t *testing.T)) bool { count++ return true }, 0) totalPath := "" for m, err := binary.ReadVarint(reader); err == nil; m, err = binary.ReadVarint(reader) { seed := int(m) index := seed % count state.Iterate(func(n string, f func(p string, t *testing.T)) bool { if index == 0 { totalPath += n + ". " f(totalPath, t) } index-- return index > 0 }, seed) for _, a := range model.Alerts().Iterate { if a.Name == "IDCollision" { t.Errorf("Path: %s Model has ID collisions", totalPath) } if a.Name == "InvalidUnitParameters" { t.Errorf("Path: %s Model units with invalid parameters", totalPath) } } } closeChan <- struct{}{} }) }