Skip to content

Commit

Permalink
Merge pull request #2 from djaglowski/trie-test-tree
Browse files Browse the repository at this point in the history
Add tree-based test suite that explores many possible sequences of operations
  • Loading branch information
VihasMakwana authored Jul 28, 2023
2 parents 412b58f + 6300e57 commit 4c918c7
Showing 1 changed file with 356 additions and 0 deletions.
356 changes: 356 additions & 0 deletions pkg/stanza/fileconsumer/internal/trie/trie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,359 @@ func runTest(t *testing.T, trie *Trie, tc *testCase) {
}
}
}

func TestTrieOpSequences(t *testing.T) {
opTree{
continuations: map[string]opTree{
"Found:ABC": opTree{ // First poll finds only ABC.
ops: []testOp{
put("ABC"),
},
continuations: map[string]opTree{
"Done:ABC": opTree{ // Finish reading ABC and remove from trie
ops: []testOp{
del("ABC", true, "was just added"),
has("ABC", false, "was just deleted"),
},
},
"Found:ABCDEF": opTree{ // Next poll finds ABCDEF
ops: []testOp{
has("ABCDEF", true, "recognize ABC w/ DEF appended"),
put("ABCDEF"), // TODO HasKey returns true, so how do we know to call this?
has("ABC", false, "should push ABC down to ABCDEF"),
},
// TODO When we started reading this file, we identified it as ABC.
// However, we have an updated understanding that it is ABCDEF.
// Should we only have to delete ABC, or only ABCDEF, or both?
continuations: map[string]opTree{
"DeleteAs:ABC": opTree{ // Done reading the file, remove it as ABC
ops: []testOp{
del("ABC", false, "should have been pushed down when ABCDEF was added"), // TODO incorrectly returns true
has("ABCDEF", true, "should not have been deleted"),
},
continuations: map[string]opTree{
"DeleteAs:ABCDEF": opTree{ // Also remove it as ABCDEF
ops: []testOp{
del("ABCDEF", true, "just confirmed it exists"), // TODO this fails to delete
},
},
},
},
"DeleteAs:ABCDEF": opTree{ // Done reading the file, remove it as ABC
ops: []testOp{
del("ABCDEF", true, "trying to delete ABC should not affect ABCDEF"),
has("ABC", true, "should not have been deleted"),
},
continuations: map[string]opTree{
"DeleteAs:ABC": opTree{ // Also remove it as ABC
ops: []testOp{
del("ABC", true, "just confirmed it exists"), // TODO this fails to delete
},
},
},
},
},
},
},
},
"Found:ABC,ABCDEF": opTree{ // First poll finds ABCDEF and ABC.
ops: []testOp{
// In order to avoid overwriting ABC with ABCDEF, we need to add ABCDEF first.
// TODO Should poll results be sorted by decreasing length before adding to trie?
put("ABCDEF"),
put("ABC"),
has("ABCDEF", true, "adding ABC after ABCDEF shouldn't affect ABCDEF"),
},
continuations: map[string]opTree{
"Done:ABC": opTree{ // Finish reading ABC and remove from trie
ops: []testOp{
del("ABC", true, "just confirmed ABC exists"),
has("ABCDEF", true, "ABCDEF should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEF": opTree{ // Finish reading ABCDEF and remove from trie
ops: []testOp{
del("ABCDEF", true, "just confirmed ABCDEF exists"),
},
},
},
},
"Done:ABCDEF": opTree{ // Finish reading ABCDEF and remove from trie
ops: []testOp{
del("ABCDEF", true, "just confirmed ABCDEF exists"),
has("ABC", true, "should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABC": opTree{ // Finish reading ABC and remove from trie
ops: []testOp{
del("ABC", true, "just confirmed ABC exists"),
},
},
},
},
"Found:ABCxyz,ABCDEF": opTree{ // Next poll finds ABCxyz and ABCDEF
ops: []testOp{
has("ABCxyz", true, "recognize ABC w/ xyz appended"),
put("ABCxyz"), // TODO how do we know we need to call this?
has("ABC", false, "ABC should have been pushed down to ABCxyz"),
has("ABCDEF", true, "ABCDEF should not have been affected"),
},
continuations: map[string]opTree{
"Done:ABCDEF": opTree{ // Finish reading ABCDEF and remove from trie
ops: []testOp{
del("ABCDEF", true, "just confirmed ABCDEF exists"),
has("ABCxyz", true, "ABCxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
},
},
},
},
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
has("ABCDEF", true, "ABCDEF should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEF": opTree{ // Finish reading ABCDEF and remove from trie
ops: []testOp{
del("ABCDEF", true, "just confirmed ABCDEF exists"),
},
},
"Found:ABCDEFxyz": opTree{ // Next poll finds ABCDEFxyz
ops: []testOp{
has("ABCDEFxyz", true, "recognize ABCDEF w/ xyz appended"),
put("ABCDEFxyz"), // TODO how do we know we need to call this?
},
continuations: map[string]opTree{
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
},
},
},
},
},
},
"Found:ABCxyz,ABCDEFxyz": opTree{ // Next poll finds ABCxyz and ABCDEFxyz
ops: []testOp{
has("ABCDEFxyz", true, "recognize ABCDEF w/ xyz appended"),
put("ABCDEFxyz"), // TODO how do we know we need to call this?
has("ABCxyz", true, "ABCxyz should not have been affected"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
has("ABCDEFxyz", true, "ABCDEFxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
},
},
},
},
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
has("ABCxyz", true, "ABCxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
},
},
},
},
},
},
},
},
"Found:ABC,ABCDEFxyz": opTree{ // Next poll finds ABC and ABCDEFxyz
ops: []testOp{
has("ABCDEFxyz", true, "recognize ABCDEF w/ xyz appended"),
put("ABCDEFxyz"), // TODO how do we know we need to call this?
has("ABCDEF", false, "ABCDEF should have been pushed down to ABCDEFxyz"),
has("ABC", true, "ABC should not have been affected"),
},
continuations: map[string]opTree{
"Done:ABC": opTree{ // Finish reading ABC and remove from trie
ops: []testOp{
del("ABC", true, "just confirmed ABC exists"),
has("ABCDEFxyz", true, "ABCDEFxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
},
},
},
},
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
has("ABC", true, "ABC should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABC": opTree{ // Finish reading ABC and remove from trie
ops: []testOp{
del("ABC", true, "just confirmed ABC exists"),
},
},
"Found:ABCxyz": opTree{ // Next poll finds ABCxyz
ops: []testOp{
has("ABCxyz", true, "recognize ABC w/ xyz appended"),
put("ABCxyz"), // TODO how do we know we need to call this?
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
},
},
},
},
},
},
"Found:ABCxyz,ABCDEFxyz": opTree{ // Next poll finds ABCxyz and ABCDEFxyz
ops: []testOp{
has("ABCxyz", true, "recognize ABC w/ xyz appended"),
put("ABCxyz"), // TODO how do we know we need to call this?
has("ABCDEFxyz", true, "ABCDEFxyz should not have been affected"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
has("ABCDEFxyz", true, "ABCDEFxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
},
},
},
},
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
has("ABCxyz", true, "ABCxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
},
},
},
},
},
},
},
},
"Found:ABCxyz,ABCDEFxyz": opTree{ // Next poll finds ABCxyz and ABCDEFxyz
ops: []testOp{
// Inserting longer string first
has("ABCDEFxyz", true, "recognize ABCDEF w/ xyz appended"),
put("ABCDEFxyz"), // TODO how do we know we need to call this?
has("ABCDEF", false, "ABCDEF should have been pushed down to ABCDEFxyz"),
has("ABC", true, "ABC should not have been affected"),

has("ABCxyz", true, "recognize ABC w/ xyz appended"),
put("ABCxyz"), // TODO how do we know we need to call this?
has("ABC", false, "ABC should have been pushed down to ABCxyz"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
has("ABCDEFxyz", true, "ABCDEFxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
},
},
},
},
"Done:ABCDEFxyz": opTree{ // Finish reading ABCDEFxyz and remove from trie
ops: []testOp{
del("ABCDEFxyz", true, "just confirmed ABCDEFxyz exists"),
has("ABCxyz", true, "ABCxyz should not have been deleted"),
},
continuations: map[string]opTree{
"Done:ABCxyz": opTree{ // Finish reading ABCxyz and remove from trie
ops: []testOp{
del("ABCxyz", true, "just confirmed ABCxyz exists"),
},
},
},
},
},
},
},
},
},
}.run(t, []testOp{})(t)
}

// testOp is one HasKey, Put, or Delete call to the trie,
// along with validation of expectations.
type testOp func(t *testing.T, trie *Trie)

func has(key string, expect bool, why string) testOp {
return func(t *testing.T, trie *Trie) {
assert.Equalf(t, trie.HasKey([]byte(key)), expect, why)
}
}

// put automatically asserts that the trie contains the key after adding.
func put(key string) testOp {
return func(t *testing.T, trie *Trie) {
trie.Put([]byte(key))
assert.Truef(t, trie.HasKey([]byte(key)), "called Put(%s) but HasKey(%s) is still false", key, key)
}
}

// del automatically asserts that the trie no longer contains the key after deleting it.
func del(key string, expect bool, why string) testOp {
return func(t *testing.T, trie *Trie) {
assert.Equalf(t, trie.Delete([]byte(key)), expect, why)
assert.Falsef(t, trie.HasKey([]byte(key)), "called Delete(%s) but HasKey(%s) is still true", key, key)
}
}

// opTree represents many possible sequences of operations that may be performed on a trie.
// Each opTree represents a stage at which a concrete sequence of operations should occur "now".
// An opTree's "continuations" are possible "futures" that may occur next, each of which may have a variety of further continuations.
// The tree structure allows us to thoroughly explore the space of possible sequences without having to define the same setup steps over and over.
type opTree struct {
ops []testOp
continuations map[string]opTree
}

func (ot opTree) run(t *testing.T, opSequence []testOp) func(*testing.T) {
return func(t *testing.T) {
trie := NewTrie()
opSequence = append(opSequence, ot.ops...)
for _, op := range opSequence {
op(t, trie)
}
if t.Failed() {
// All continuations will fail at the same point, so don't bother running them
return
}
for name, continuation := range ot.continuations {
t.Run(name, continuation.run(t, opSequence))
}
}
}

0 comments on commit 4c918c7

Please sign in to comment.