├── examples_test.ExampleVerifyString.approved.txt ├── go.mod ├── testdata ├── approvals_test.TestParameterizedTests.Short.approved.txt ├── approvals_test.TestReporterFromSetup.approved.txt ├── approvals_test.TestVerifyMapEmptyMap.approved.txt ├── approvals_test.TestVerifyStringApproval.approved.txt ├── approvals_test.TestParameterizedTests.Normal.approved.txt ├── approvals_test.TestVerifyArrayEmptyArray.approved.txt ├── approvals_test.TestVerifyMap.approved.txt ├── approvals_test.TestParameterizedTests.Long.approved.txt ├── approvals_test.TestVerifyArray.approved.txt ├── approvals_test.TestParameterizedTests.Composed_name.approved.txt ├── approvals_test.TestVerifyMapBadMap.approved.txt ├── approvals_test.TestVerifyJSONBytes.approved.json ├── approvals_test.TestVerifyArrayBadArray.approved.txt ├── approvals_test.TestVerifyArrayTransformation.approved.txt ├── approvals_test.TestVerifyJSONStruct.approved.json ├── approvals_test.TestVerifyAllCombinationsFor1.approved.txt ├── approvals_test.TestVerifyXMLBytes.approved.xml ├── approvals_test.TestVerifyXMLStruct.approved.xml ├── approvals_test.TestVerifyAllCombinationsFor2.approved.txt ├── approvals_test.TestVerifyBadXMLBytes.approved.xml ├── approvals_test.TestVerifyAllCombinationsForSkipped.approved.txt ├── approvals_test.TestVerifyBadJSONBytes.approved.json ├── approvals_test.TestVerifyBadXMLStruct.approved.xml └── approvals_test.TestVerifyAllCombinationsFor9.approved.txt ├── run_tests.bat ├── utils ├── doc.go ├── testing_utils.go ├── file_utils.go └── collection_utils.go ├── doc.go ├── reporters ├── constants.go ├── doc.go ├── all_failing.go ├── filemerge.go ├── real_diff_reporter.go ├── newbie.go ├── continuous_integration.go ├── goland.go ├── file_launcher.go ├── intellij.go ├── beyond_compare.go ├── quiet.go ├── vscode.go ├── diff_reporter.go ├── reporter.go ├── clipboard.go └── reporter_test.go ├── examples_test.ExampleVerifyAllCombinationsFor2_withSkip.approved.txt ├── examples_test.ExampleVerifyAllCombinationsFor2.approved.txt ├── .gitignore ├── TODO.md ├── install.windows.ps1 ├── .github └── workflows │ └── test.yml ├── namer_test.go ├── .golangci.yml ├── approval_name_test.go ├── examples_helper_test.go ├── README.md ├── examples_test.go ├── reporter_test.go ├── approval_name.go ├── approvals_test.go ├── approvals.go ├── combination_approvals.go └── LICENSE.md /examples_test.ExampleVerifyString.approved.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/approvals/go-approval-tests 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestParameterizedTests.Short.approved.txt: -------------------------------------------------------------------------------- 1 | Hello A! -------------------------------------------------------------------------------- /testdata/approvals_test.TestReporterFromSetup.approved.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyMapEmptyMap.approved.txt: -------------------------------------------------------------------------------- 1 | len(map) == 0 -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyStringApproval.approved.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /testdata/approvals_test.TestParameterizedTests.Normal.approved.txt: -------------------------------------------------------------------------------- 1 | Hello Sue! -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyArrayEmptyArray.approved.txt: -------------------------------------------------------------------------------- 1 | len(array) == 0 -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyMap.approved.txt: -------------------------------------------------------------------------------- 1 | [cat]=meow 2 | [dog]=bark -------------------------------------------------------------------------------- /testdata/approvals_test.TestParameterizedTests.Long.approved.txt: -------------------------------------------------------------------------------- 1 | Hello Chandrasekhar! -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyArray.approved.txt: -------------------------------------------------------------------------------- 1 | [0]=dog 2 | [1]=cat 3 | [2]=bird -------------------------------------------------------------------------------- /testdata/approvals_test.TestParameterizedTests.Composed_name.approved.txt: -------------------------------------------------------------------------------- 1 | Hello Karl-Martin! -------------------------------------------------------------------------------- /run_tests.bat: -------------------------------------------------------------------------------- 1 | go build -v ./... 2 | go test ./... -race -coverprofile=coverage.txt -covermode=atomic -------------------------------------------------------------------------------- /utils/doc.go: -------------------------------------------------------------------------------- 1 | // Package utils contains functions supporting approval testing. 2 | package utils 3 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyMapBadMap.approved.txt: -------------------------------------------------------------------------------- 1 | error while printing map 2 | received a string 3 | foo 4 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyJSONBytes.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 42, 3 | "bark": "woof", 4 | "foo": "bar" 5 | } -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyArrayBadArray.approved.txt: -------------------------------------------------------------------------------- 1 | error while printing array 2 | received a string 3 | string 4 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyArrayTransformation.approved.txt: -------------------------------------------------------------------------------- 1 | uppercase 2 | 3 | 4 | Christopher => CHRISTOPHER 5 | Llewellyn => LLEWELLYN -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyJSONStruct.approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Hello World!", 3 | "Name": "Peter Pan", 4 | "Age": 100 5 | } -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyAllCombinationsFor1.approved.txt: -------------------------------------------------------------------------------- 1 | uppercase 2 | 3 | 4 | [Christopher] => CHRISTOPHER 5 | [Llewellyn] => LLEWELLYN -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package approvals allows for easy testing of larger objects that can be saved to a file (images, sounds, csv, etc...) 2 | package approvals 3 | -------------------------------------------------------------------------------- /reporters/constants.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | const ( 4 | goosWindows = "windows" 5 | goosDarwin = "darwin" 6 | goosLinux = "linux" 7 | ) 8 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyXMLBytes.approved.xml: -------------------------------------------------------------------------------- 1 | 2 | Hello World! 3 | Peter Pan 4 | 100 5 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyXMLStruct.approved.xml: -------------------------------------------------------------------------------- 1 | 2 | Hello World! 3 | Peter Pan 4 | 100 5 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyAllCombinationsFor2.approved.txt: -------------------------------------------------------------------------------- 1 | character at 2 | 3 | 4 | [Christopher,0] => C 5 | [Christopher,1] => h 6 | [Llewellyn,0] => L 7 | [Llewellyn,1] => l -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyBadXMLBytes.approved.xml: -------------------------------------------------------------------------------- 1 | error while parsing XML 2 | error: 3 | XML syntax error on line 1: unexpected end element 4 | XML: 5 | Test> 6 | -------------------------------------------------------------------------------- /examples_test.ExampleVerifyAllCombinationsFor2_withSkip.approved.txt: -------------------------------------------------------------------------------- 1 | combineWords 2 | 3 | 4 | [stack,trickle] => stacktrickle 5 | [fold,overflow] => foldoverflow 6 | [fold,trickle] => foldtrickle -------------------------------------------------------------------------------- /examples_test.ExampleVerifyAllCombinationsFor2.approved.txt: -------------------------------------------------------------------------------- 1 | substring 2 | 3 | 4 | [aaaaa,2] => aa 5 | [aaaaa,3] => aaa 6 | [bbbbb,2] => bb 7 | [bbbbb,3] => bbb 8 | [ccccc,2] => cc 9 | [ccccc,3] => ccc -------------------------------------------------------------------------------- /reporters/doc.go: -------------------------------------------------------------------------------- 1 | // Package reporters provides types to report comparison results. 2 | // 3 | // Reporters launch programs on failure to help you understand, fix and approve results. 4 | package reporters 5 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyAllCombinationsForSkipped.approved.txt: -------------------------------------------------------------------------------- 1 | skipped divisible by 3 2 | 3 | 4 | [1] => 1 5 | [2] => 2 6 | [4] => 4 7 | [5] => 5 8 | [7] => 7 9 | [8] => 8 10 | [10] => 10 -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyBadJSONBytes.approved.json: -------------------------------------------------------------------------------- 1 | error while parsing JSON 2 | error: 3 | invalid character 'f' looking for beginning of object key string 4 | JSON: 5 | { foo: "bar", "age": 42, "bark": "woof" } 6 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyBadXMLStruct.approved.xml: -------------------------------------------------------------------------------- 1 | error while pretty printing XML 2 | when using anonymous types be sure to include 3 | XMLName xml.Name `xml:"Your_Name_Here"` 4 | error: 5 | xml: unsupported type: struct { Title string } 6 | XML: 7 | {Hello World!} 8 | -------------------------------------------------------------------------------- /utils/testing_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | // AssertEqual Example: 6 | // AssertEqual(t, 10, number, "number") 7 | func AssertEqual(t *testing.T, expected, actual interface{}, message string) { 8 | if expected != actual { 9 | t.Fatalf(message+"\n[expected != actual]\n[%s != %s]", expected, actual) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.received.* 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | .idea/ 8 | *.iml 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | coverage.txt 30 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] Newbie Reporters ? 2 | - [ ] VScode reporter 3 | - [ ] Move fixtures to their own folder 4 | - [ ] Add support for an image reporter 5 | - [x] README link to go doc 6 | - [x] setup codecov.io + link in README 7 | - [ ] clean up the func VerifyAllCombinationsForX calls 8 | - [ ] move examples to testable examples 9 | - [ ] revisit documentation 10 | - [x] evaluate using https://golang.org/pkg/testing/#T.Name over getApprovalName() 11 | - [x] should support parallel tests 12 | -------------------------------------------------------------------------------- /utils/file_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | // DoesFileExist checks if a file exists. 9 | func DoesFileExist(fileName string) bool { 10 | _, err := os.Stat(fileName) 11 | return !os.IsNotExist(err) 12 | } 13 | 14 | // EnsureExists creates if the file does not already exist. 15 | func EnsureExists(fileName string) { 16 | if DoesFileExist(fileName) { 17 | return 18 | } 19 | 20 | ioutil.WriteFile(fileName, []byte(""), 0644) 21 | } 22 | -------------------------------------------------------------------------------- /install.windows.ps1: -------------------------------------------------------------------------------- 1 | # How to run this file: 2 | # iwr -useb https://raw.githubusercontent.com/approvals/go-approval-tests/main/install.windows.ps1 | iex 3 | 4 | iwr -useb https://raw.githubusercontent.com/JayBazuzi/machine-setup/main/windows.ps1 | iex 5 | iwr -useb https://raw.githubusercontent.com/JayBazuzi/machine-setup/main/golang-goland.ps1 | iex 6 | 7 | 8 | & "C:\Program Files\Git\cmd\git.exe" clone https://github.com/approvals/go-approval-tests.git C:\Code\go-approval-tests 9 | cd C:\Code\go-approval-tests 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build and Test 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | 10 | build-and-test: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Go environment 18 | uses: actions/setup-go@v2.2.0 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | 23 | - name: test 24 | run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic 25 | -------------------------------------------------------------------------------- /reporters/all_failing.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | var ( 4 | clipboardScratchData = "" 5 | ) 6 | 7 | type allFailing struct{} 8 | 9 | // NewAllFailingTestReporter copies move file command to your clipboard 10 | func NewAllFailingTestReporter() Reporter { 11 | return &allFailing{} 12 | } 13 | 14 | func (s *allFailing) Report(approved, received string) bool { 15 | move := getMoveCommandText(approved, received) 16 | clipboardScratchData = clipboardScratchData + move + "\n" 17 | return copyToClipboard(clipboardScratchData) 18 | } 19 | -------------------------------------------------------------------------------- /reporters/filemerge.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | type fileMerge struct{} 4 | 5 | // NewFileMergeReporter creates a new reporter for the Xcode filemerge diff tool. 6 | func NewFileMergeReporter() Reporter { 7 | return &fileMerge{} 8 | } 9 | 10 | func (s *fileMerge) Report(approved, received string) bool { 11 | xs := []string{"--nosplash", "-left", received, "-right", approved} 12 | programName := "/Applications/Xcode.app/Contents/Applications/FileMerge.app/Contents/MacOS/FileMerge" 13 | 14 | return launchProgram(programName, approved, xs...) 15 | } 16 | -------------------------------------------------------------------------------- /reporters/real_diff_reporter.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/approvals/go-approval-tests/utils" 8 | ) 9 | 10 | type realDiff struct{} 11 | 12 | // NewRealDiffReporter creates a reporter for the 'diff' utility. 13 | func NewRealDiffReporter() Reporter { 14 | return &realDiff{} 15 | } 16 | 17 | func (*realDiff) Report(approved, received string) bool { 18 | utils.EnsureExists(approved) 19 | 20 | cmd := exec.Command("diff", "-u", approved, received) 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | cmd.Run() 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /reporters/newbie.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type printSupportedDiffPrograms struct{} 8 | 9 | // NewPrintSupportedDiffProgramsReporter creates a new reporter that states what reporters are supported. 10 | func NewPrintSupportedDiffProgramsReporter() Reporter { 11 | return &printSupportedDiffPrograms{} 12 | } 13 | 14 | func (s *printSupportedDiffPrograms) Report(approved, received string) bool { 15 | fmt.Printf(`no diff reporters found on your system 16 | currently supported reporters are [in order of preference]: 17 | Beyond Compare 18 | IntelliJ 19 | `) 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /namer_test.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func Test00(t *testing.T) { 9 | approvalName := getApprovalName(t) 10 | 11 | approvalFile := approvalName.getApprovalFile(".txt") 12 | assertEndsWith(approvalFile, "namer_test.Test00.approved.txt", t) 13 | 14 | receivedFile := approvalName.getReceivedFile(".txt") 15 | assertEndsWith(receivedFile, "namer_test.Test00.received.txt", t) 16 | } 17 | 18 | func assertEndsWith(s string, ending string, t *testing.T) { 19 | if !strings.HasSuffix(s, ending) { 20 | t.Fatalf("expected name to be '%s', but got %s", ending, s) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | 4 | linters: 5 | enable-all: true 6 | 7 | # linters are disabled if their majority of issues is considered false-positive (intended code) 8 | # and the remaining issues (if existing) aren't worth it. 9 | disable: 10 | - gosec 11 | - errcheck 12 | - gochecknoglobals 13 | - wsl 14 | 15 | issues: 16 | exclude-use-default: false # disable filtering of defaults for better zero-issue policy 17 | max-per-linter: 0 # disable limit; report all issues of a linter 18 | max-same-issues: 0 # disable limit; report all issues of the same issue 19 | 20 | linters-settings: 21 | lll: 22 | line-length: 160 23 | -------------------------------------------------------------------------------- /reporters/continuous_integration.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | type continuousIntegration struct{} 9 | 10 | // NewContinuousIntegrationReporter creates a new reporter for CI. 11 | // 12 | // The reporter checks the environment variable CI for a value of true. 13 | func NewContinuousIntegrationReporter() Reporter { 14 | return &continuousIntegration{} 15 | } 16 | 17 | func (s *continuousIntegration) Report(approved, received string) bool { 18 | value, exists := os.LookupEnv("CI") 19 | 20 | if exists { 21 | ci, err := strconv.ParseBool(value) 22 | if err == nil { 23 | return ci 24 | } 25 | } 26 | 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /reporters/goland.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import "runtime" 4 | 5 | type goland struct{} 6 | 7 | // NewGoLandReporter creates a new reporter for Goland. 8 | func NewGoLandReporter() Reporter { 9 | return &goland{} 10 | } 11 | 12 | func (s *goland) Report(approved, received string) bool { 13 | xs := []string{"diff", received, approved} 14 | var programName string 15 | switch runtime.GOOS { 16 | case goosWindows: 17 | programName = "unknown" 18 | case goosDarwin: 19 | programName = "/Applications/GoLand.app/Contents/MacOS/goland" 20 | case goosLinux: 21 | programName = "/usr/local/bin/goland" 22 | } 23 | 24 | return launchProgram(programName, approved, xs...) 25 | } 26 | -------------------------------------------------------------------------------- /reporters/file_launcher.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | ) 7 | 8 | type fileLauncher struct{} 9 | 10 | // NewFileLauncherReporter launches registered application of the received file's type only. 11 | func NewFileLauncherReporter() Reporter { 12 | return &fileLauncher{} 13 | } 14 | 15 | func (s *fileLauncher) Report(approved, received string) bool { 16 | var cmd *exec.Cmd 17 | 18 | switch runtime.GOOS { 19 | case goosWindows: 20 | cmd = exec.Command("cmd", "/C", "start", "Needed Title", received, "/B") 21 | case goosLinux: 22 | return false 23 | default: 24 | cmd = exec.Command("open", received) 25 | } 26 | 27 | cmd.Start() 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /reporters/intellij.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import "runtime" 4 | 5 | type intellij struct{} 6 | 7 | // NewIntelliJReporter creates a new reporter for IntelliJ. 8 | func NewIntelliJReporter() Reporter { 9 | return &intellij{} 10 | } 11 | 12 | func (s *intellij) Report(approved, received string) bool { 13 | xs := []string{"diff", received, approved} 14 | 15 | var programName string 16 | switch runtime.GOOS { 17 | case goosWindows: 18 | programName = "C:/Program Files (x86)/JetBrains/IntelliJ IDEA 2016/bin/idea.exe" 19 | case goosDarwin: 20 | programName = "/Applications/IntelliJ IDEA.app/Contents/MacOS/idea" 21 | case goosLinux: 22 | programName = "/usr/local/bin/idea" 23 | } 24 | 25 | return launchProgram(programName, approved, xs...) 26 | } 27 | -------------------------------------------------------------------------------- /reporters/beyond_compare.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | type beyondCompare struct{} 8 | 9 | // NewBeyondCompareReporter creates a new reporter for Beyond Compare 4. 10 | func NewBeyondCompareReporter() Reporter { 11 | return &beyondCompare{} 12 | } 13 | 14 | func (s *beyondCompare) Report(approved, received string) bool { 15 | xs := []string{received, approved} 16 | var programName string 17 | switch runtime.GOOS { 18 | case goosWindows: 19 | programName = "C:/Program Files/Beyond Compare 4/BComp.exe" 20 | case goosDarwin: 21 | programName = "/Applications/Beyond Compare.app/Contents/MacOS/bcomp" 22 | case goosLinux: 23 | programName = "/usr/bin/bcompare" 24 | } 25 | 26 | return launchProgram(programName, approved, xs...) 27 | } 28 | -------------------------------------------------------------------------------- /reporters/quiet.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/approvals/go-approval-tests/utils" 8 | ) 9 | 10 | type quiet struct{} 11 | 12 | // NewQuietReporter creates a new reporter that does nothing. 13 | func NewQuietReporter() Reporter { 14 | return &quiet{} 15 | } 16 | 17 | func (s *quiet) Report(approved, received string) bool { 18 | approvedFull, _ := filepath.Abs(approved) 19 | receivedFull, _ := filepath.Abs(received) 20 | 21 | if utils.DoesFileExist(approved) { 22 | fmt.Printf("approval files did not match\napproved: %v\nreceived: %v\n", approvedFull, receivedFull) 23 | } else { 24 | fmt.Printf("result never approved\napproved: %v\nreceived: %v\n", approvedFull, receivedFull) 25 | } 26 | 27 | return true 28 | } 29 | -------------------------------------------------------------------------------- /reporters/vscode.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | ) 8 | 9 | type vsCode struct{} 10 | 11 | // NewVSCodeReporter creates a new reporter for the Visual Studio Code diff tool. 12 | func NewVSCodeReporter() Reporter { 13 | return &vsCode{} 14 | } 15 | 16 | func (s *vsCode) Report(approved, received string) bool { 17 | xs := []string{"-d", received, approved} 18 | var programName string 19 | switch runtime.GOOS { 20 | case goosWindows: 21 | if username, ok := os.LookupEnv("USERNAME"); ok { 22 | programName = fmt.Sprintf("C:/Users/%s/AppData/Local/Programs/Microsoft VS Code/Code.exe", username) 23 | } 24 | case goosDarwin: 25 | programName = "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" 26 | case goosLinux: 27 | programName = "/usr/bin/code" 28 | } 29 | 30 | return launchProgram(programName, approved, xs...) 31 | } 32 | -------------------------------------------------------------------------------- /reporters/diff_reporter.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/approvals/go-approval-tests/utils" 7 | ) 8 | 9 | // NewFrontLoadedReporter creates the default front loaded reporter. 10 | func NewFrontLoadedReporter() Reporter { 11 | return NewFirstWorkingReporter( 12 | NewContinuousIntegrationReporter(), 13 | ) 14 | } 15 | 16 | // NewDiffReporter creates the default diff reporter. 17 | func NewDiffReporter() Reporter { 18 | return NewFirstWorkingReporter( 19 | NewBeyondCompareReporter(), 20 | NewIntelliJReporter(), 21 | NewFileMergeReporter(), 22 | NewVSCodeReporter(), 23 | NewGoLandReporter(), 24 | NewRealDiffReporter(), 25 | NewPrintSupportedDiffProgramsReporter(), 26 | NewQuietReporter(), 27 | ) 28 | } 29 | 30 | func launchProgram(programName, approved string, args ...string) bool { 31 | if !utils.DoesFileExist(programName) { 32 | return false 33 | } 34 | 35 | utils.EnsureExists(approved) 36 | 37 | cmd := exec.Command(programName, args...) 38 | cmd.Start() 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /approval_name_test.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNamer(t *testing.T) { 9 | var namer = getApprovalName(t) 10 | if !strings.HasSuffix(namer.name, "TestNamer") { 11 | t.Fatalf("test name is wrong in namer, got %s", namer.name) 12 | } 13 | } 14 | 15 | func TestNamerFilename(t *testing.T) { 16 | var namer = getApprovalName(t) 17 | if !strings.HasSuffix(namer.fileName, "approval_name_test.go") { 18 | t.Fatalf("test filename is wrong in namer, got %s", namer.fileName) 19 | } 20 | } 21 | 22 | func TestParameterizedTestNames(t *testing.T) { 23 | for _, tc := range ExampleParameterizedTestcases { 24 | tc := tc 25 | t.Run(tc.name, func(t *testing.T) { 26 | var namer = getApprovalName(t) 27 | sanitizedName := strings.Replace(tc.name, " ", "_", -1) 28 | if !strings.Contains(namer.name, "TestParameterizedTestNames.") && 29 | !strings.HasSuffix(namer.name, sanitizedName) { 30 | t.Fatalf("parameterized test name is wrong in namer, got %s", namer.name) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples_helper_test.go: -------------------------------------------------------------------------------- 1 | // nolint:unused // this is an example file 2 | package approvals_test 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "strings" 9 | 10 | approvals "github.com/approvals/go-approval-tests" 11 | ) 12 | 13 | var ( 14 | // this is a mock testing.T for documentation purposes 15 | t = &approvals.TestFailable{} 16 | ) 17 | 18 | // failing is a mock struct that is only there for documentation convenience, 19 | // showing the developer how they would be passing a *testing.T pointer in their 20 | // normal tests. 21 | type failing struct{} 22 | 23 | // Fail implements approvaltest.Fail 24 | func (f *failing) Fail() {} 25 | 26 | // documentation helper just for the example 27 | func printFileContent(path string) { 28 | approvedPath := strings.Replace(path, ".received.", ".approved.", 1) 29 | content, err := ioutil.ReadFile(approvedPath) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | fmt.Printf("This produced the file %s\n", path) 34 | fmt.Printf("It will be compared against the %s file\n", approvedPath) 35 | fmt.Println("and contains the text:") 36 | fmt.Println() 37 | // sad sad hack because go examples trim blank middle lines 38 | cleaned_text := strings.Replace(string(content), "\r", "", -1) 39 | cleaned_text = strings.Replace(cleaned_text, "\n\n", "\n", -1) 40 | fmt.Println(cleaned_text) 41 | } 42 | -------------------------------------------------------------------------------- /reporters/reporter.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | // Reporter are called on failing approvals. 4 | type Reporter interface { 5 | // Report is called when the approved and received file do not match. 6 | Report(approved, received string) bool 7 | } 8 | 9 | // FirstWorkingReporter reports using the first possible reporter. 10 | type FirstWorkingReporter struct { 11 | Reporters []Reporter 12 | } 13 | 14 | // Report is called when the approved and received file do not match. 15 | func (s *FirstWorkingReporter) Report(approved, received string) bool { 16 | for _, reporter := range s.Reporters { 17 | result := reporter.Report(approved, received) 18 | if result { 19 | return true 20 | } 21 | } 22 | 23 | return false 24 | } 25 | 26 | // NewFirstWorkingReporter creates in the order reporters are passed in. 27 | func NewFirstWorkingReporter(reporters ...Reporter) Reporter { 28 | return &FirstWorkingReporter{ 29 | Reporters: reporters, 30 | } 31 | } 32 | 33 | // MultiReporter reports all reporters. 34 | type MultiReporter struct { 35 | Reporters []Reporter 36 | } 37 | 38 | // Report is called when the approved and received file do not match. 39 | func (s *MultiReporter) Report(approved, received string) bool { 40 | result := false 41 | for _, reporter := range s.Reporters { 42 | result = reporter.Report(approved, received) || result 43 | } 44 | 45 | return result 46 | } 47 | 48 | // NewMultiReporter calls all reporters. 49 | func NewMultiReporter(reporters ...Reporter) Reporter { 50 | return &MultiReporter{ 51 | Reporters: reporters, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /reporters/clipboard.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | type clipboard struct{} 11 | 12 | // NewClipboardReporter copies move file command to your clipboard 13 | func NewClipboardReporter() Reporter { 14 | return &clipboard{} 15 | } 16 | 17 | func (s *clipboard) Report(approved, received string) bool { 18 | move := getMoveCommandText(approved, received) 19 | return copyToClipboard(move) 20 | } 21 | 22 | func copyToClipboard(move string) bool { 23 | switch runtime.GOOS { 24 | case goosWindows: 25 | return copyToWindowsClipboard(move) 26 | default: 27 | return copyToDarwinClipboard(move) 28 | } 29 | } 30 | 31 | func getMoveCommandText(approved, received string) string { 32 | receivedFull, _ := filepath.Abs(received) 33 | approvedFull, _ := filepath.Abs(approved) 34 | 35 | var move string 36 | 37 | switch runtime.GOOS { 38 | case goosWindows: 39 | move = fmt.Sprintf("move /Y \"%s\" \"%s\"", receivedFull, approvedFull) 40 | default: 41 | move = fmt.Sprintf("mv %s %s", receivedFull, approvedFull) 42 | } 43 | 44 | return move 45 | } 46 | func copyToWindowsClipboard(text string) bool { 47 | return pipeToProgram("clip", text) 48 | } 49 | 50 | func copyToDarwinClipboard(text string) bool { 51 | return pipeToProgram("pbcopy", text) 52 | } 53 | 54 | func pipeToProgram(programName, text string) bool { 55 | c := exec.Command(programName) 56 | pipe, err := c.StdinPipe() 57 | if err != nil { 58 | fmt.Printf("StdinPipe: err=%s", err) 59 | return false 60 | } 61 | pipe.Write([]byte(text)) 62 | pipe.Close() 63 | 64 | c.Start() 65 | return true 66 | } 67 | -------------------------------------------------------------------------------- /utils/collection_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // PrintMap prints a map 11 | func PrintMap(m interface{}) string { 12 | var outputText string 13 | 14 | v := reflect.ValueOf(m) 15 | if v.Kind() != reflect.Map { 16 | outputText = fmt.Sprintf("error while printing map\nreceived a %T\n %s\n", m, m) 17 | } else { 18 | keys := v.MapKeys() 19 | var xs []string 20 | 21 | for _, k := range keys { 22 | xs = append(xs, fmt.Sprintf("[%s]=%s", k, v.MapIndex(k))) 23 | } 24 | 25 | sort.Strings(xs) 26 | if len(xs) == 0 { 27 | outputText = "len(map) == 0" 28 | } else { 29 | outputText = strings.Join(xs, "\n") 30 | } 31 | } 32 | 33 | return outputText 34 | } 35 | 36 | // PrintArray prints an array 37 | func PrintArray(m interface{}) string { 38 | var outputText string 39 | 40 | switch reflect.TypeOf(m).Kind() { 41 | case reflect.Slice: 42 | var xs []string 43 | 44 | slice := reflect.ValueOf(m) 45 | for i := 0; i < slice.Len(); i++ { 46 | xs = append(xs, fmt.Sprintf("[%d]=%s", i, slice.Index(i))) 47 | } 48 | 49 | if len(xs) == 0 { 50 | outputText = "len(array) == 0" 51 | } else { 52 | outputText = strings.Join(xs, "\n") 53 | } 54 | default: 55 | outputText = fmt.Sprintf("error while printing array\nreceived a %T\n %s\n", m, m) 56 | } 57 | 58 | return outputText 59 | } 60 | 61 | // MapToString maps a collection to a string collection 62 | func MapToString(collection interface{}, transform func(x interface{}) string) []string { 63 | switch reflect.TypeOf(collection).Kind() { 64 | case reflect.Slice: 65 | var xs []string 66 | 67 | slice := reflect.ValueOf(collection) 68 | for i := 0; i < slice.Len(); i++ { 69 | xs = append(xs, transform(slice.Index(i).Interface())) 70 | } 71 | 72 | return xs 73 | default: 74 | panic(fmt.Sprintf("error while mapping array to string\nreceived a %T\n %s\n", collection, collection)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /reporters/reporter_test.go: -------------------------------------------------------------------------------- 1 | package reporters 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/approvals/go-approval-tests/utils" 8 | ) 9 | 10 | type testReporter struct { 11 | called bool 12 | succeeded bool 13 | } 14 | 15 | func newTestReporter(succeeded bool) *testReporter { 16 | return &testReporter{ 17 | called: false, 18 | succeeded: succeeded, 19 | } 20 | } 21 | 22 | func (s *testReporter) Report(approved, received string) bool { 23 | s.called = true 24 | return s.succeeded 25 | } 26 | 27 | func TestFirstWorkingReporter(t *testing.T) { 28 | a := newTestReporter(false) 29 | b := newTestReporter(true) 30 | c := newTestReporter(true) 31 | 32 | testSubject := NewFirstWorkingReporter(Reporter(a), Reporter(b), Reporter(c)) 33 | testSubject.Report("a.txt", "b.txt") 34 | 35 | utils.AssertEqual(t, true, a.called, "a.called") 36 | utils.AssertEqual(t, true, b.called, "b.called") 37 | utils.AssertEqual(t, false, c.called, "c.called") 38 | } 39 | 40 | func TestMultiReporter(t *testing.T) { 41 | a := newTestReporter(true) 42 | b := newTestReporter(true) 43 | 44 | testSubject := NewMultiReporter(Reporter(a), Reporter(b)) 45 | result := testSubject.Report("a.txt", "b.txt") 46 | 47 | utils.AssertEqual(t, true, a.called, "a.called") 48 | utils.AssertEqual(t, true, b.called, "b.called") 49 | utils.AssertEqual(t, true, result, "result") 50 | } 51 | 52 | func TestMultiReporterWithNoWorkingReporters(t *testing.T) { 53 | a := newTestReporter(false) 54 | b := newTestReporter(false) 55 | 56 | testSubject := NewMultiReporter(Reporter(a), Reporter(b)) 57 | result := testSubject.Report("a.txt", "b.txt") 58 | 59 | utils.AssertEqual(t, true, a.called, "a.called") 60 | utils.AssertEqual(t, true, b.called, "b.called") 61 | utils.AssertEqual(t, false, result, "result") 62 | } 63 | 64 | func restoreEnv(exists bool, key, value string) { 65 | if exists { 66 | os.Setenv(key, value) 67 | } else { 68 | os.Unsetenv(key) 69 | } 70 | } 71 | 72 | func TestCIReporter(t *testing.T) { 73 | value, exists := os.LookupEnv("CI") 74 | 75 | os.Setenv("CI", "true") 76 | defer restoreEnv(exists, "CI", value) 77 | 78 | r := NewContinuousIntegrationReporter() 79 | utils.AssertEqual(t, true, r.Report("", ""), "did not detect CI") 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApprovalTests.go 2 | 3 | ApprovalTests for [go](https://golang.org/) 4 | 5 | [![GoDoc](https://godoc.org/github.com/approvals/go-approval-tests?status.svg)](https://godoc.org/github.com/approvals/go-approval-tests) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/approvals/go-approval-tests)](https://goreportcard.com/report/github.com/approvals/go-approval-tests) 7 | [![Coverage Status](https://codecov.io/gh/approvals/go-approval-tests/graph/badge.svg)](https://codecov.io/gh/approvals/go-approval-tests) 8 | [![Build and Test](https://github.com/approvals/go-approval-tests/actions/workflows/test.yml/badge.svg)](https://github.com/approvals/go-approval-tests/actions/workflows/test.yml) 9 | 10 | # Golden master Verification Library 11 | 12 | ApprovalTests allows for easy testing of larger objects, strings and anything else that can be saved to a file (images, sounds, csv, etc...) 13 | 14 | # Examples 15 | ## Basic string verification 16 | 17 | ```go 18 | func TestHelloWorld(t *testing.T) { 19 | approvals.VerifyString(t, "Hello World!") 20 | } 21 | ``` 22 | 23 | ## Store approved files in testdata subfolder 24 | Some people prefer to store their approved files in a subfolder "testdata" instead of in the same folder as the 25 | production code. To configure this, add a call to UseFolder to your TestMain: 26 | 27 | ```go 28 | func TestMain(m *testing.M) { 29 | UseFolder("testdata") 30 | os.Exit(m.Run()) 31 | } 32 | ``` 33 | 34 | ## In Project 35 | Note: ApprovalTests uses approvals to test itself. Therefore there are many examples in the code itself. 36 | 37 | - [approvals_test.go](approvals_test.go) 38 | 39 | ## JSON 40 | VerifyJSONBytes - Simple Formatting for easy comparison. Also uses the .json file extension 41 | 42 | ```go 43 | func TestVerifyJSON(t *testing.T) { 44 | jsonb := []byte("{ \"foo\": \"bar\", \"age\": 42, \"bark\": \"woof\" }") 45 | VerifyJSONBytes(t, jsonb) 46 | } 47 | ``` 48 | 49 | Matches file: approvals_test.TestVerifyJSON.received.json 50 | 51 | ```json 52 | { 53 | "age": 42, 54 | "bark": "woof", 55 | "foo": "bar" 56 | } 57 | ``` 58 | 59 | ## Reporters 60 | ApprovalTests becomes _much_ more powerful with reporters. Reporters launch programs on failure to help you understand, fix and approve results. 61 | 62 | You can make your own easily, [here's an example](reporters/beyond_compare.go) 63 | You can also declare which one to use. Either at the 64 | 65 | ### Method level 66 | 67 | ```go 68 | r := UseReporter(reporters.NewIntelliJ()) 69 | defer r.Close() 70 | ``` 71 | 72 | ### Test Level 73 | 74 | ```go 75 | func TestMain(m *testing.M) { 76 | r := UseReporter(reporters.NewBeyondCompareReporter()) 77 | defer r.Close() 78 | UseFolder("testdata") 79 | 80 | os.Exit(m.Run()) 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package approvals_test 2 | 3 | import ( 4 | approvals "github.com/approvals/go-approval-tests" 5 | ) 6 | 7 | func ExampleVerifyString() { 8 | approvals.VerifyString(t, "Hello World!") 9 | printFileContent("examples_test.ExampleVerifyString.received.txt") 10 | 11 | // Output: 12 | // This produced the file examples_test.ExampleVerifyString.received.txt 13 | // It will be compared against the examples_test.ExampleVerifyString.approved.txt file 14 | // and contains the text: 15 | // 16 | // Hello World! 17 | } 18 | 19 | func ExampleVerifyAllCombinationsFor2() { 20 | t = makeExamplesRunLikeTests("ExampleVerifyAllCombinationsFor2") 21 | 22 | letters := []string{"aaaaa", "bbbbb", "ccccc"} 23 | numbers := []int{2, 3} 24 | 25 | functionToTest := func(text interface{}, length interface{}) string { 26 | return text.(string)[:length.(int)] 27 | } 28 | 29 | approvals.VerifyAllCombinationsFor2(t, "substring", functionToTest, letters, numbers) 30 | printFileContent("examples_test.ExampleVerifyAllCombinationsFor2.received.txt") 31 | // Output: 32 | // This produced the file examples_test.ExampleVerifyAllCombinationsFor2.received.txt 33 | // It will be compared against the examples_test.ExampleVerifyAllCombinationsFor2.approved.txt file 34 | // and contains the text: 35 | // 36 | // substring 37 | // 38 | // 39 | // [aaaaa,2] => aa 40 | // [aaaaa,3] => aaa 41 | // [bbbbb,2] => bb 42 | // [bbbbb,3] => bbb 43 | // [ccccc,2] => cc 44 | // [ccccc,3] => ccc 45 | } 46 | 47 | func makeExamplesRunLikeTests(name string) *approvals.TestFailable { 48 | t = approvals.NewTestFailableWithName(name) 49 | approvals.UseFolder("") 50 | return t 51 | } 52 | 53 | func ExampleVerifyAllCombinationsFor2_withSkip() { 54 | t = makeExamplesRunLikeTests("ExampleVerifyAllCombinationsFor2_withSkip") 55 | 56 | words := []string{"stack", "fold"} 57 | otherWords := []string{"overflow", "trickle"} 58 | 59 | functionToTest := func(firstWord interface{}, secondWord interface{}) string { 60 | first := firstWord.(string) 61 | second := secondWord.(string) 62 | if first+second == "stackoverflow" { 63 | return approvals.SkipThisCombination 64 | } 65 | return first + second 66 | } 67 | 68 | approvals.VerifyAllCombinationsFor2(t, "combineWords", functionToTest, words, otherWords) 69 | printFileContent("examples_test.ExampleVerifyAllCombinationsFor2_withSkip.received.txt") 70 | // Output: 71 | // This produced the file examples_test.ExampleVerifyAllCombinationsFor2_withSkip.received.txt 72 | // It will be compared against the examples_test.ExampleVerifyAllCombinationsFor2_withSkip.approved.txt file 73 | // and contains the text: 74 | // 75 | // combineWords 76 | // 77 | // [stack,trickle] => stacktrickle 78 | // [fold,overflow] => foldoverflow 79 | // [fold,trickle] => foldtrickle 80 | } 81 | -------------------------------------------------------------------------------- /reporter_test.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/approvals/go-approval-tests/reporters" 8 | "github.com/approvals/go-approval-tests/utils" 9 | ) 10 | 11 | // TestFailable is a fake replacing testing.T 12 | // It implements the parts of the testing.T interface approvals uses, 13 | // ie the approvaltests.Failable interface 14 | type TestFailable struct { 15 | name string 16 | } 17 | 18 | func (s *TestFailable) Fail() {} 19 | func (s *TestFailable) Name() string { 20 | return s.name 21 | 22 | } 23 | func (s *TestFailable) Fatalf(format string, args ...interface{}) {} 24 | func (s *TestFailable) Fatal(args ...interface{}) {} 25 | func (s *TestFailable) Log(args ...interface{}) {} 26 | func (s *TestFailable) Logf(format string, args ...interface{}) {} 27 | func (s *TestFailable) Helper() {} 28 | 29 | func NewTestFailable() *TestFailable { 30 | return &TestFailable{ 31 | name: "TestFailable", 32 | } 33 | } 34 | func NewTestFailableWithName(name string) *TestFailable { 35 | return &TestFailable{ 36 | name: name, 37 | } 38 | } 39 | 40 | type testReporter struct { 41 | called bool 42 | succeeded bool 43 | } 44 | 45 | func newTestReporter(succeeded bool) *testReporter { 46 | return &testReporter{ 47 | called: false, 48 | succeeded: succeeded, 49 | } 50 | } 51 | 52 | func (s *testReporter) Report(approved, received string) bool { 53 | s.called = true 54 | os.Remove(received) 55 | 56 | return s.succeeded 57 | } 58 | 59 | func TestUseReporter(t *testing.T) { 60 | front := UseFrontLoadedReporter(newTestReporter(false)) 61 | defer front.Close() 62 | 63 | old := getReporter() 64 | a := newTestReporter(true) 65 | r := UseReporter(reporters.Reporter(a)) 66 | 67 | f := &TestFailable{} 68 | 69 | VerifyString(f, "foo") 70 | 71 | utils.AssertEqual(t, true, a.called, "a.called") 72 | r.Close() 73 | 74 | current := getReporter() 75 | 76 | oldT, _ := old.(*reporters.FirstWorkingReporter) 77 | currentT, _ := current.(*reporters.FirstWorkingReporter) 78 | 79 | utils.AssertEqual(t, oldT.Reporters[1], currentT.Reporters[1], "reporters[1]") 80 | } 81 | 82 | func TestFrontLoadedReporter(t *testing.T) { 83 | old := getReporter() 84 | front := newTestReporter(false) 85 | next := newTestReporter(true) 86 | 87 | frontCloser := UseFrontLoadedReporter(reporters.Reporter(front)) 88 | nextCloser := UseReporter(reporters.Reporter(next)) 89 | defer nextCloser.Close() 90 | 91 | f := &TestFailable{} 92 | 93 | VerifyString(f, "foo") 94 | 95 | utils.AssertEqual(t, true, front.called, "front.called") 96 | utils.AssertEqual(t, true, next.called, "next.called") 97 | 98 | frontCloser.Close() 99 | current := getReporter() 100 | 101 | oldT, _ := old.(*reporters.FirstWorkingReporter) 102 | currentT, _ := current.(*reporters.FirstWorkingReporter) 103 | 104 | utils.AssertEqual(t, oldT.Reporters[0], currentT.Reporters[0], "reporters[0]") 105 | } 106 | -------------------------------------------------------------------------------- /approval_name.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | // ApprovalName struct. 15 | type ApprovalName struct { 16 | name string 17 | fileName string 18 | } 19 | 20 | func getApprovalName(t Failable) *ApprovalName { 21 | fileName, err := findFileName() 22 | if err != nil { 23 | t.Fatalf("approvals: could not find the test filename or approved files location") 24 | return nil 25 | } 26 | 27 | var name = t.Name() 28 | name = strings.ReplaceAll(name, "/", ".") 29 | namer := NewApprovalName(name, *fileName) 30 | 31 | return &namer 32 | } 33 | 34 | // NewApprovalName returns a new ApprovalName object. 35 | func NewApprovalName(name string, fileName string) ApprovalName { 36 | var namer = ApprovalName{ 37 | name: name, 38 | fileName: fileName, 39 | } 40 | return namer 41 | } 42 | 43 | // Walk the call stack, and try to find the test method that was executed. 44 | // The test method is identified by looking for the test runner, which is 45 | // *assumed* to be common across all callers. The test runner has a Name() of 46 | // 'testing.tRunner'. The method immediately previous to this is the test 47 | // method. 48 | func findFileName() (*string, error) { 49 | pc := make([]uintptr, 100) 50 | count := runtime.Callers(0, pc) 51 | 52 | i := 0 53 | var lastFunc *runtime.Func 54 | 55 | for ; i < count; i++ { 56 | lastFunc = runtime.FuncForPC(pc[i]) 57 | if isTestRunner(lastFunc) { 58 | break 59 | } 60 | } 61 | testMethodPtr := pc[i-1] 62 | testMethod := runtime.FuncForPC(testMethodPtr) 63 | var fileName, _ = testMethod.FileLine(testMethodPtr) 64 | 65 | if i == 0 || !isTestRunner(lastFunc) { 66 | return nil, fmt.Errorf("approvals: could not find the test method") 67 | } 68 | return &fileName, nil 69 | } 70 | 71 | func isTestRunner(f *runtime.Func) bool { 72 | return f != nil && f.Name() == "testing.tRunner" || f.Name() == "testing.runExample" 73 | } 74 | 75 | func (s *ApprovalName) compare(approvalFile, receivedFile string, reader io.Reader) error { 76 | received, err := ioutil.ReadAll(reader) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // Ideally, this should only be written if 82 | // 1. the approval file does not exist 83 | // 2. the results differ 84 | err = s.dumpReceivedTestResult(received, receivedFile) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | fh, err := os.Open(approvalFile) 90 | if err != nil { 91 | return err 92 | } 93 | defer fh.Close() 94 | 95 | approved, err := ioutil.ReadAll(fh) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | received = s.normalizeLineEndings(received) 101 | approved = s.normalizeLineEndings(approved) 102 | 103 | // The two sides are identical, nothing more to do. 104 | if bytes.Equal(received, approved) { 105 | return nil 106 | } 107 | 108 | return fmt.Errorf("failed to approved %s", s.name) 109 | } 110 | 111 | func (s *ApprovalName) normalizeLineEndings(bs []byte) []byte { 112 | return bytes.Replace(bs, []byte("\r\n"), []byte("\n"), -1) 113 | } 114 | 115 | func (s *ApprovalName) dumpReceivedTestResult(bs []byte, receivedFile string) error { 116 | err := ioutil.WriteFile(receivedFile, bs, 0644) 117 | 118 | return err 119 | } 120 | 121 | func (s *ApprovalName) getFileName(extWithDot string, suffix string) string { 122 | if !strings.HasPrefix(extWithDot, ".") { 123 | extWithDot = fmt.Sprintf(".%s", extWithDot) 124 | } 125 | 126 | _, baseName := path.Split(s.fileName) 127 | baseWithoutExt := baseName[:len(baseName)-len(path.Ext(s.fileName))] 128 | 129 | filename := fmt.Sprintf("%s.%s.%s%s", baseWithoutExt, s.name, suffix, extWithDot) 130 | 131 | return path.Join(defaultFolder, filename) 132 | } 133 | 134 | func (s *ApprovalName) getReceivedFile(extWithDot string) string { 135 | return s.getFileName(extWithDot, "received") 136 | } 137 | 138 | func (s *ApprovalName) getApprovalFile(extWithDot string) string { 139 | return s.getFileName(extWithDot, "approved") 140 | } 141 | -------------------------------------------------------------------------------- /approvals_test.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/approvals/go-approval-tests/reporters" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | r := UseReporter(reporters.NewContinuousIntegrationReporter()) 15 | defer r.Close() 16 | 17 | UseFolder("testdata") 18 | 19 | os.Exit(m.Run()) 20 | } 21 | 22 | func TestVerifyStringApproval(t *testing.T) { 23 | r := UseReporter(reporters.NewContinuousIntegrationReporter()) 24 | defer r.Close() 25 | 26 | VerifyString(t, "Hello World!") 27 | } 28 | 29 | func TestReporterFromSetup(t *testing.T) { 30 | VerifyString(t, hello("World")) 31 | } 32 | 33 | type ExampleTestCaseParameters struct { 34 | name string 35 | value string 36 | } 37 | 38 | var ExampleParameterizedTestcases = []ExampleTestCaseParameters{ 39 | {name: "Normal", value: "Sue"}, 40 | {name: "Long", value: "Chandrasekhar"}, 41 | {name: "Short", value: "A"}, 42 | {name: "Composed name", value: "Karl-Martin"}, 43 | } 44 | 45 | // hello world function that can be the system-under-test 46 | func hello(name string) string { 47 | return fmt.Sprintf("Hello %s!", name) 48 | } 49 | 50 | func TestParameterizedTests(t *testing.T) { 51 | t.Parallel() 52 | for _, tc := range ExampleParameterizedTestcases { 53 | tc := tc 54 | t.Run(tc.name, func(t *testing.T) { 55 | VerifyString(t, hello(tc.value)) 56 | }) 57 | } 58 | } 59 | 60 | func TestVerifyXMLStruct(t *testing.T) { 61 | json := struct { 62 | XMLName xml.Name `xml:"Test"` 63 | Title string 64 | Name string 65 | Age int 66 | }{ 67 | Title: "Hello World!", 68 | Name: "Peter Pan", 69 | Age: 100, 70 | } 71 | 72 | VerifyXMLStruct(t, json) 73 | } 74 | 75 | func TestVerifyBadXMLStruct(t *testing.T) { 76 | xmlContent := struct { 77 | Title string 78 | }{ 79 | Title: "Hello World!", 80 | } 81 | 82 | VerifyXMLStruct(t, xmlContent) 83 | } 84 | 85 | func TestVerifyXMLBytes(t *testing.T) { 86 | xmlb := []byte("Hello World!Peter Pan100") 87 | VerifyXMLBytes(t, xmlb) 88 | } 89 | 90 | func TestVerifyBadXMLBytes(t *testing.T) { 91 | xmlb := []byte("Test>") 92 | VerifyXMLBytes(t, xmlb) 93 | } 94 | 95 | func TestVerifyJSONStruct(t *testing.T) { 96 | json := struct { 97 | Title string 98 | Name string 99 | Age int 100 | }{ 101 | Title: "Hello World!", 102 | Name: "Peter Pan", 103 | Age: 100, 104 | } 105 | 106 | VerifyJSONStruct(t, json) 107 | } 108 | 109 | func TestVerifyJSONBytes(t *testing.T) { 110 | jsonb := []byte("{ \"foo\": \"bar\", \"age\": 42, \"bark\": \"woof\" }") 111 | VerifyJSONBytes(t, jsonb) 112 | } 113 | 114 | func TestVerifyBadJSONBytes(t *testing.T) { 115 | jsonb := []byte("{ foo: \"bar\", \"age\": 42, \"bark\": \"woof\" }") 116 | VerifyJSONBytes(t, jsonb) 117 | } 118 | 119 | func TestVerifyMap(t *testing.T) { 120 | m := map[string]string{ 121 | "dog": "bark", 122 | "cat": "meow", 123 | } 124 | 125 | VerifyMap(t, m) 126 | } 127 | 128 | func TestVerifyMapBadMap(t *testing.T) { 129 | m := "foo" 130 | VerifyMap(t, m) 131 | } 132 | 133 | func TestVerifyMapEmptyMap(t *testing.T) { 134 | m := map[string]string{} 135 | VerifyMap(t, m) 136 | } 137 | 138 | func TestVerifyArray(t *testing.T) { 139 | xs := []string{"dog", "cat", "bird"} 140 | VerifyArray(t, xs) 141 | } 142 | 143 | func TestVerifyArrayBadArray(t *testing.T) { 144 | xs := "string" 145 | VerifyArray(t, xs) 146 | } 147 | 148 | func TestVerifyArrayEmptyArray(t *testing.T) { 149 | var xs []string 150 | VerifyArray(t, xs) 151 | } 152 | 153 | func TestVerifyArrayTransformation(t *testing.T) { 154 | xs := []string{"Christopher", "Llewellyn"} 155 | VerifyAll(t, "uppercase", xs, func(x interface{}) string { return fmt.Sprintf("%s => %s", x, strings.ToUpper(x.(string))) }) 156 | } 157 | 158 | func TestVerifyAllCombinationsFor1(t *testing.T) { 159 | xs := []string{"Christopher", "Llewellyn"} 160 | VerifyAllCombinationsFor1(t, "uppercase", func(x interface{}) string { return strings.ToUpper(x.(string)) }, xs) 161 | } 162 | 163 | func TestVerifyAllCombinationsForSkipped(t *testing.T) { 164 | xs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 165 | VerifyAllCombinationsFor1( 166 | t, 167 | "skipped divisible by 3", 168 | func(x interface{}) string { 169 | if x.(int)%3 == 0 { 170 | return SkipThisCombination 171 | } 172 | return fmt.Sprintf("%v", x) 173 | }, 174 | xs) 175 | } 176 | 177 | func TestVerifyAllCombinationsFor2(t *testing.T) { 178 | xs1 := []string{"Christopher", "Llewellyn"} 179 | xs2 := []int{0, 1} 180 | VerifyAllCombinationsFor2( 181 | t, 182 | "character at", 183 | func(s interface{}, i interface{}) string { return fmt.Sprintf("%c", s.(string)[i.(int)]) }, 184 | xs1, 185 | xs2) 186 | } 187 | 188 | func TestVerifyAllCombinationsFor9(t *testing.T) { 189 | xs1 := []string{"Christopher"} 190 | 191 | VerifyAllCombinationsFor9( 192 | t, 193 | "sum numbers", 194 | func(s, i2, i3, i4, i5, i6, i7, i8, i9 interface{}) string { 195 | sum := i2.(int) + i3.(int) + i4.(int) + i5.(int) + i6.(int) + i7.(int) + i8.(int) + i9.(int) 196 | return fmt.Sprintf("%v[%v]", s, sum) 197 | }, 198 | xs1, 199 | []int{0, 1}, 200 | []int{2, 3}, 201 | []int{4, 5}, 202 | []int{6, 7}, 203 | []int{8, 9}, 204 | []int{10, 11}, 205 | []int{12, 13}, 206 | []int{14, 15}) 207 | } 208 | -------------------------------------------------------------------------------- /approvals.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "fmt" 8 | "io" 9 | "os" 10 | "reflect" 11 | "strings" 12 | 13 | "github.com/approvals/go-approval-tests/reporters" 14 | "github.com/approvals/go-approval-tests/utils" 15 | ) 16 | 17 | var ( 18 | defaultReporter = reporters.NewDiffReporter() 19 | defaultFrontLoadedReporter = reporters.NewFrontLoadedReporter() 20 | defaultFolder = "" 21 | ) 22 | 23 | // Failable is an interface wrapper around testing.T 24 | type Failable interface { 25 | Fail() 26 | Fatal(args ...interface{}) 27 | Fatalf(format string, args ...interface{}) 28 | Name() string 29 | Log(args ...interface{}) 30 | Logf(format string, args ...interface{}) 31 | Helper() 32 | } 33 | 34 | // VerifyWithExtension Example: 35 | // VerifyWithExtension(t, strings.NewReader("Hello"), ".txt") 36 | func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string) { 37 | t.Helper() 38 | namer := getApprovalName(t) 39 | 40 | reporter := getReporter() 41 | var err = namer.compare(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot), reader) 42 | if err != nil { 43 | reporter.Report(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot)) 44 | t.Log("Failed Approval: received does not match approved.") 45 | t.Fail() 46 | } else { 47 | _ = os.Remove(namer.getReceivedFile(extWithDot)) 48 | } 49 | } 50 | 51 | // Verify Example: 52 | // Verify(t, strings.NewReader("Hello")) 53 | func Verify(t Failable, reader io.Reader) { 54 | t.Helper() 55 | VerifyWithExtension(t, reader, ".txt") 56 | } 57 | 58 | // VerifyString stores the passed string into the received file and confirms 59 | // that it matches the approved local file. On failure, it will launch a reporter. 60 | func VerifyString(t Failable, s string) { 61 | t.Helper() 62 | reader := strings.NewReader(s) 63 | Verify(t, reader) 64 | } 65 | 66 | // VerifyXMLStruct Example: 67 | // VerifyXMLStruct(t, xml) 68 | func VerifyXMLStruct(t Failable, obj interface{}) { 69 | t.Helper() 70 | xmlContent, err := xml.MarshalIndent(obj, "", " ") 71 | if err != nil { 72 | tip := "" 73 | if reflect.TypeOf(obj).Name() == "" { 74 | tip = "when using anonymous types be sure to include\n XMLName xml.Name `xml:\"Your_Name_Here\"`\n" 75 | } 76 | message := fmt.Sprintf("error while pretty printing XML\n%verror:\n %v\nXML:\n %v\n", tip, err, obj) 77 | VerifyWithExtension(t, strings.NewReader(message), ".xml") 78 | } else { 79 | VerifyWithExtension(t, bytes.NewReader(xmlContent), ".xml") 80 | } 81 | } 82 | 83 | // VerifyXMLBytes Example: 84 | // VerifyXMLBytes(t, []byte("")) 85 | func VerifyXMLBytes(t Failable, bs []byte) { 86 | t.Helper() 87 | type node struct { 88 | Attr []xml.Attr 89 | XMLName xml.Name 90 | Children []node `xml:",any"` 91 | Text string `xml:",chardata"` 92 | } 93 | x := node{} 94 | 95 | err := xml.Unmarshal(bs, &x) 96 | if err != nil { 97 | message := fmt.Sprintf("error while parsing XML\nerror:\n %s\nXML:\n %s\n", err, string(bs)) 98 | VerifyWithExtension(t, strings.NewReader(message), ".xml") 99 | } else { 100 | VerifyXMLStruct(t, x) 101 | } 102 | } 103 | 104 | // VerifyJSONStruct Example: 105 | // VerifyJSONStruct(t, json) 106 | func VerifyJSONStruct(t Failable, obj interface{}) { 107 | t.Helper() 108 | jsonb, err := json.MarshalIndent(obj, "", " ") 109 | if err != nil { 110 | message := fmt.Sprintf("error while pretty printing JSON\nerror:\n %s\nJSON:\n %s\n", err, obj) 111 | VerifyWithExtension(t, strings.NewReader(message), ".json") 112 | } else { 113 | VerifyWithExtension(t, bytes.NewReader(jsonb), ".json") 114 | } 115 | } 116 | 117 | // VerifyJSONBytes Example: 118 | // VerifyJSONBytes(t, []byte("{ \"Greeting\": \"Hello\" }")) 119 | func VerifyJSONBytes(t Failable, bs []byte) { 120 | t.Helper() 121 | var obj map[string]interface{} 122 | err := json.Unmarshal(bs, &obj) 123 | if err != nil { 124 | message := fmt.Sprintf("error while parsing JSON\nerror:\n %s\nJSON:\n %s\n", err, string(bs)) 125 | VerifyWithExtension(t, strings.NewReader(message), ".json") 126 | } else { 127 | VerifyJSONStruct(t, obj) 128 | } 129 | } 130 | 131 | // VerifyMap Example: 132 | // VerifyMap(t, map[string][string] { "dog": "bark" }) 133 | func VerifyMap(t Failable, m interface{}) { 134 | t.Helper() 135 | outputText := utils.PrintMap(m) 136 | VerifyString(t, outputText) 137 | } 138 | 139 | // VerifyArray Example: 140 | // VerifyArray(t, []string{"dog", "cat"}) 141 | func VerifyArray(t Failable, array interface{}) { 142 | t.Helper() 143 | outputText := utils.PrintArray(array) 144 | VerifyString(t, outputText) 145 | } 146 | 147 | // VerifyAll Example: 148 | // VerifyAll(t, "uppercase", []string("dog", "cat"}, func(x interface{}) string { return strings.ToUpper(x.(string)) }) 149 | func VerifyAll(t Failable, header string, collection interface{}, transform func(interface{}) string) { 150 | t.Helper() 151 | if len(header) != 0 { 152 | header = fmt.Sprintf("%s\n\n\n", header) 153 | } 154 | 155 | outputText := header + strings.Join(utils.MapToString(collection, transform), "\n") 156 | VerifyString(t, outputText) 157 | } 158 | 159 | type reporterCloser struct { 160 | reporter reporters.Reporter 161 | } 162 | 163 | func (s *reporterCloser) Close() error { 164 | defaultReporter = s.reporter 165 | return nil 166 | } 167 | 168 | type frontLoadedReporterCloser struct { 169 | reporter reporters.Reporter 170 | } 171 | 172 | func (s *frontLoadedReporterCloser) Close() error { 173 | defaultFrontLoadedReporter = s.reporter 174 | return nil 175 | } 176 | 177 | // UseReporter configures which reporter to use on failure. 178 | // Add at the test or method level to configure your reporter. 179 | // 180 | // The following examples shows how to use a reporter for all of your test cases 181 | // in a package directory through go's setup feature. 182 | // 183 | // 184 | // func TestMain(m *testing.M) { 185 | // r := approvals.UseReporter(reporters.NewBeyondCompareReporter()) 186 | // defer r.Close() 187 | // 188 | // os.Exit(m.Run()) 189 | // } 190 | // 191 | func UseReporter(reporter reporters.Reporter) io.Closer { 192 | closer := &reporterCloser{ 193 | reporter: defaultReporter, 194 | } 195 | 196 | defaultReporter = reporter 197 | return closer 198 | } 199 | 200 | // UseFrontLoadedReporter configures reporters ahead of all other reporters to 201 | // handle situations like CI. These reporters usually prevent reporting in 202 | // scenarios that are headless. 203 | func UseFrontLoadedReporter(reporter reporters.Reporter) io.Closer { 204 | closer := &frontLoadedReporterCloser{ 205 | reporter: defaultFrontLoadedReporter, 206 | } 207 | 208 | defaultFrontLoadedReporter = reporter 209 | return closer 210 | } 211 | 212 | func getReporter() reporters.Reporter { 213 | return reporters.NewFirstWorkingReporter( 214 | defaultFrontLoadedReporter, 215 | defaultReporter, 216 | ) 217 | } 218 | 219 | // UseFolder configures which folder to use to store approval files. 220 | // By default, the approval files will be stored at the same level as the code. 221 | // 222 | // The following examples shows how to use the idiomatic 'testdata' folder 223 | // for all of your test cases in a package directory. 224 | // 225 | // 226 | // func TestMain(m *testing.M) { 227 | // approvals.UseFolder("testdata") 228 | // 229 | // os.Exit(m.Run()) 230 | // } 231 | // 232 | func UseFolder(f string) { 233 | defaultFolder = f 234 | } 235 | -------------------------------------------------------------------------------- /combination_approvals.go: -------------------------------------------------------------------------------- 1 | package approvals 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type emptyType struct{} 10 | 11 | // SkipThisCombination should be returned if you do not want to process a particular combination. 12 | const SkipThisCombination = "♬ SKIP THIS COMBINATION ♬" 13 | 14 | var ( 15 | empty = emptyType{} 16 | emptyCollection = []emptyType{empty} 17 | ) 18 | 19 | // VerifyAllCombinationsFor1 Example: 20 | // VerifyAllCombinationsFor1(t, "uppercase", func(x interface{}) string { return strings.ToUpper(x.(string)) }, []string("dog", "cat"}) 21 | func VerifyAllCombinationsFor1(t Failable, header string, transform func(interface{}) string, collection1 interface{}) { 22 | transform2 := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 23 | return transform(p1) 24 | } 25 | 26 | VerifyAllCombinationsFor9(t, header, transform2, collection1, 27 | emptyCollection, 28 | emptyCollection, 29 | emptyCollection, 30 | emptyCollection, 31 | emptyCollection, 32 | emptyCollection, 33 | emptyCollection, 34 | emptyCollection) 35 | } 36 | 37 | // VerifyAllCombinationsFor2 calls the transform function with all combinations 38 | // from collection 1 and collection 2. The resulting received file contains all 39 | // inputs and the resulting outputs. The received file is then compared to the 40 | // approved version. If the transform function returns SkipThisCombination the 41 | // output of this combination won't be displayed inside the received file. 42 | func VerifyAllCombinationsFor2( 43 | t Failable, 44 | header string, 45 | transform func(p1, p2 interface{}) string, 46 | collection1, collection2 interface{}) { 47 | transform2 := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 48 | return transform(p1, p2) 49 | } 50 | 51 | VerifyAllCombinationsFor9(t, header, transform2, collection1, 52 | collection2, 53 | emptyCollection, 54 | emptyCollection, 55 | emptyCollection, 56 | emptyCollection, 57 | emptyCollection, 58 | emptyCollection, 59 | emptyCollection) 60 | } 61 | 62 | // VerifyAllCombinationsFor3 is for combinations of 3. 63 | func VerifyAllCombinationsFor3( 64 | t Failable, 65 | header string, 66 | transform func(p1, p2, p3 interface{}) string, 67 | collection1, collection2, collection3 interface{}) { 68 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 69 | return transform(p1, p2, p3) 70 | } 71 | 72 | VerifyAllCombinationsFor9(t, header, kerning, 73 | collection1, 74 | collection2, 75 | collection3, 76 | emptyCollection, 77 | emptyCollection, 78 | emptyCollection, 79 | emptyCollection, 80 | emptyCollection, 81 | emptyCollection) 82 | } 83 | 84 | // VerifyAllCombinationsFor4 is for combinations of 4. 85 | func VerifyAllCombinationsFor4( 86 | t Failable, 87 | header string, 88 | transform func(p1, p2, p3, p4 interface{}) string, 89 | collection1, collection2, collection3, collection4 interface{}) { 90 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 91 | return transform(p1, p2, p3, p4) 92 | } 93 | 94 | VerifyAllCombinationsFor9(t, header, kerning, 95 | collection1, 96 | collection2, 97 | collection3, 98 | collection4, 99 | emptyCollection, 100 | emptyCollection, 101 | emptyCollection, 102 | emptyCollection, 103 | emptyCollection) 104 | } 105 | 106 | // VerifyAllCombinationsFor5 is for combinations of 5. 107 | func VerifyAllCombinationsFor5( 108 | t Failable, 109 | header string, 110 | transform func(p1, p2, p3, p4, p5 interface{}) string, 111 | collection1, collection2, collection3, collection4, collection5 interface{}) { 112 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 113 | return transform(p1, p2, p3, p4, p5) 114 | } 115 | 116 | VerifyAllCombinationsFor9(t, header, kerning, 117 | collection1, 118 | collection2, 119 | collection3, 120 | collection4, 121 | collection5, 122 | emptyCollection, 123 | emptyCollection, 124 | emptyCollection, 125 | emptyCollection) 126 | } 127 | 128 | // VerifyAllCombinationsFor6 is for combinations of 6. 129 | func VerifyAllCombinationsFor6( 130 | t Failable, 131 | header string, 132 | transform func(p1, p2, p3, p4, p5, p6 interface{}) string, 133 | collection1, collection2, collection3, collection4, collection5, collection6 interface{}) { 134 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 135 | return transform(p1, p2, p3, p4, p5, p6) 136 | } 137 | 138 | VerifyAllCombinationsFor9(t, header, kerning, 139 | collection1, 140 | collection2, 141 | collection3, 142 | collection4, 143 | collection5, 144 | collection6, 145 | emptyCollection, 146 | emptyCollection, 147 | emptyCollection) 148 | } 149 | 150 | // VerifyAllCombinationsFor7 is for combinations of 7. 151 | func VerifyAllCombinationsFor7( 152 | t Failable, 153 | header string, 154 | transform func(p1, p2, p3, p4, p5, p6, p7 interface{}) string, 155 | collection1, collection2, collection3, collection4, collection5, collection6, collection7 interface{}) { 156 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 157 | return transform(p1, p2, p3, p4, p5, p6, p7) 158 | } 159 | 160 | VerifyAllCombinationsFor9(t, header, kerning, 161 | collection1, 162 | collection2, 163 | collection3, 164 | collection4, 165 | collection5, 166 | collection6, 167 | collection7, 168 | emptyCollection, 169 | emptyCollection) 170 | } 171 | 172 | // VerifyAllCombinationsFor8 is for combinations of 8. 173 | func VerifyAllCombinationsFor8( 174 | t Failable, 175 | header string, 176 | transform func(p1, p2, p3, p4, p5, p6, p7, p8 interface{}) string, 177 | collection1, collection2, collection3, collection4, collection5, collection6, collection7, collection8 interface{}) { 178 | kerning := func(p1, p2, p3, p4, p5, p6, p7, p8, p9 interface{}) string { 179 | return transform(p1, p2, p3, p4, p5, p6, p7, p8) 180 | } 181 | 182 | VerifyAllCombinationsFor9(t, header, kerning, 183 | collection1, 184 | collection2, 185 | collection3, 186 | collection4, 187 | collection5, 188 | collection6, 189 | collection7, 190 | collection8, 191 | emptyCollection) 192 | } 193 | 194 | // VerifyAllCombinationsFor9 is for combinations of 9. 195 | func VerifyAllCombinationsFor9( // nolint: funlen, gocognit 196 | t Failable, 197 | header string, 198 | transform func(a, b, c, d, e, f, g, h, i interface{}) string, 199 | collection1, 200 | collection2, 201 | collection3, 202 | collection4, 203 | collection5, 204 | collection6, 205 | collection7, 206 | collection8, 207 | collection9 interface{}) { 208 | if len(header) != 0 { 209 | header = fmt.Sprintf("%s\n\n\n", header) 210 | } 211 | 212 | var mapped []string 213 | slice1 := reflect.ValueOf(collection1) 214 | slice2 := reflect.ValueOf(collection2) 215 | slice3 := reflect.ValueOf(collection3) 216 | slice4 := reflect.ValueOf(collection4) 217 | slice5 := reflect.ValueOf(collection5) 218 | slice6 := reflect.ValueOf(collection6) 219 | slice7 := reflect.ValueOf(collection7) 220 | slice8 := reflect.ValueOf(collection8) 221 | slice9 := reflect.ValueOf(collection9) 222 | 223 | for i1 := 0; i1 < slice1.Len(); i1++ { 224 | for i2 := 0; i2 < slice2.Len(); i2++ { 225 | for i3 := 0; i3 < slice3.Len(); i3++ { 226 | for i4 := 0; i4 < slice4.Len(); i4++ { 227 | for i5 := 0; i5 < slice5.Len(); i5++ { 228 | for i6 := 0; i6 < slice6.Len(); i6++ { 229 | for i7 := 0; i7 < slice7.Len(); i7++ { 230 | for i8 := 0; i8 < slice8.Len(); i8++ { 231 | for i9 := 0; i9 < slice9.Len(); i9++ { 232 | p1 := slice1.Index(i1).Interface() 233 | p2 := slice2.Index(i2).Interface() 234 | p3 := slice3.Index(i3).Interface() 235 | p4 := slice4.Index(i4).Interface() 236 | p5 := slice5.Index(i5).Interface() 237 | p6 := slice6.Index(i6).Interface() 238 | p7 := slice7.Index(i7).Interface() 239 | p8 := slice8.Index(i8).Interface() 240 | p9 := slice9.Index(i9).Interface() 241 | 242 | parameterText := getParameterText(p1, p2, p3, p4, p5, p6, p7, p8, p9) 243 | transformText := getTransformText(transform, p1, p2, p3, p4, p5, p6, p7, p8, p9) 244 | if transformText != SkipThisCombination { 245 | mapped = append(mapped, fmt.Sprintf("%s => %s", parameterText, transformText)) 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | outputText := header + strings.Join(mapped, "\n") 258 | VerifyString(t, outputText) 259 | } 260 | 261 | func getParameterText(args ...interface{}) string { 262 | parameterText := "[" 263 | for _, x := range args { 264 | if x != empty { 265 | parameterText += fmt.Sprintf("%v,", x) 266 | } 267 | } 268 | 269 | parameterText = parameterText[0 : len(parameterText)-1] 270 | parameterText += "]" 271 | 272 | return parameterText 273 | } 274 | 275 | func getTransformText( 276 | transform func(a, b, c, d, e, f, g, h, i interface{}) string, 277 | p1, 278 | p2, 279 | p3, 280 | p4, 281 | p5, 282 | p6, 283 | p7, 284 | p8, 285 | p9 interface{}) (s string) { 286 | defer func() { 287 | r := recover() 288 | if r != nil { 289 | s = "panic occurred" 290 | } 291 | }() 292 | 293 | return transform(p1, p2, p3, p4, p5, p6, p7, p8, p9) 294 | } 295 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /testdata/approvals_test.TestVerifyAllCombinationsFor9.approved.txt: -------------------------------------------------------------------------------- 1 | sum numbers 2 | 3 | 4 | [Christopher,0,2,4,6,8,10,12,14] => Christopher[56] 5 | [Christopher,0,2,4,6,8,10,12,15] => Christopher[57] 6 | [Christopher,0,2,4,6,8,10,13,14] => Christopher[57] 7 | [Christopher,0,2,4,6,8,10,13,15] => Christopher[58] 8 | [Christopher,0,2,4,6,8,11,12,14] => Christopher[57] 9 | [Christopher,0,2,4,6,8,11,12,15] => Christopher[58] 10 | [Christopher,0,2,4,6,8,11,13,14] => Christopher[58] 11 | [Christopher,0,2,4,6,8,11,13,15] => Christopher[59] 12 | [Christopher,0,2,4,6,9,10,12,14] => Christopher[57] 13 | [Christopher,0,2,4,6,9,10,12,15] => Christopher[58] 14 | [Christopher,0,2,4,6,9,10,13,14] => Christopher[58] 15 | [Christopher,0,2,4,6,9,10,13,15] => Christopher[59] 16 | [Christopher,0,2,4,6,9,11,12,14] => Christopher[58] 17 | [Christopher,0,2,4,6,9,11,12,15] => Christopher[59] 18 | [Christopher,0,2,4,6,9,11,13,14] => Christopher[59] 19 | [Christopher,0,2,4,6,9,11,13,15] => Christopher[60] 20 | [Christopher,0,2,4,7,8,10,12,14] => Christopher[57] 21 | [Christopher,0,2,4,7,8,10,12,15] => Christopher[58] 22 | [Christopher,0,2,4,7,8,10,13,14] => Christopher[58] 23 | [Christopher,0,2,4,7,8,10,13,15] => Christopher[59] 24 | [Christopher,0,2,4,7,8,11,12,14] => Christopher[58] 25 | [Christopher,0,2,4,7,8,11,12,15] => Christopher[59] 26 | [Christopher,0,2,4,7,8,11,13,14] => Christopher[59] 27 | [Christopher,0,2,4,7,8,11,13,15] => Christopher[60] 28 | [Christopher,0,2,4,7,9,10,12,14] => Christopher[58] 29 | [Christopher,0,2,4,7,9,10,12,15] => Christopher[59] 30 | [Christopher,0,2,4,7,9,10,13,14] => Christopher[59] 31 | [Christopher,0,2,4,7,9,10,13,15] => Christopher[60] 32 | [Christopher,0,2,4,7,9,11,12,14] => Christopher[59] 33 | [Christopher,0,2,4,7,9,11,12,15] => Christopher[60] 34 | [Christopher,0,2,4,7,9,11,13,14] => Christopher[60] 35 | [Christopher,0,2,4,7,9,11,13,15] => Christopher[61] 36 | [Christopher,0,2,5,6,8,10,12,14] => Christopher[57] 37 | [Christopher,0,2,5,6,8,10,12,15] => Christopher[58] 38 | [Christopher,0,2,5,6,8,10,13,14] => Christopher[58] 39 | [Christopher,0,2,5,6,8,10,13,15] => Christopher[59] 40 | [Christopher,0,2,5,6,8,11,12,14] => Christopher[58] 41 | [Christopher,0,2,5,6,8,11,12,15] => Christopher[59] 42 | [Christopher,0,2,5,6,8,11,13,14] => Christopher[59] 43 | [Christopher,0,2,5,6,8,11,13,15] => Christopher[60] 44 | [Christopher,0,2,5,6,9,10,12,14] => Christopher[58] 45 | [Christopher,0,2,5,6,9,10,12,15] => Christopher[59] 46 | [Christopher,0,2,5,6,9,10,13,14] => Christopher[59] 47 | [Christopher,0,2,5,6,9,10,13,15] => Christopher[60] 48 | [Christopher,0,2,5,6,9,11,12,14] => Christopher[59] 49 | [Christopher,0,2,5,6,9,11,12,15] => Christopher[60] 50 | [Christopher,0,2,5,6,9,11,13,14] => Christopher[60] 51 | [Christopher,0,2,5,6,9,11,13,15] => Christopher[61] 52 | [Christopher,0,2,5,7,8,10,12,14] => Christopher[58] 53 | [Christopher,0,2,5,7,8,10,12,15] => Christopher[59] 54 | [Christopher,0,2,5,7,8,10,13,14] => Christopher[59] 55 | [Christopher,0,2,5,7,8,10,13,15] => Christopher[60] 56 | [Christopher,0,2,5,7,8,11,12,14] => Christopher[59] 57 | [Christopher,0,2,5,7,8,11,12,15] => Christopher[60] 58 | [Christopher,0,2,5,7,8,11,13,14] => Christopher[60] 59 | [Christopher,0,2,5,7,8,11,13,15] => Christopher[61] 60 | [Christopher,0,2,5,7,9,10,12,14] => Christopher[59] 61 | [Christopher,0,2,5,7,9,10,12,15] => Christopher[60] 62 | [Christopher,0,2,5,7,9,10,13,14] => Christopher[60] 63 | [Christopher,0,2,5,7,9,10,13,15] => Christopher[61] 64 | [Christopher,0,2,5,7,9,11,12,14] => Christopher[60] 65 | [Christopher,0,2,5,7,9,11,12,15] => Christopher[61] 66 | [Christopher,0,2,5,7,9,11,13,14] => Christopher[61] 67 | [Christopher,0,2,5,7,9,11,13,15] => Christopher[62] 68 | [Christopher,0,3,4,6,8,10,12,14] => Christopher[57] 69 | [Christopher,0,3,4,6,8,10,12,15] => Christopher[58] 70 | [Christopher,0,3,4,6,8,10,13,14] => Christopher[58] 71 | [Christopher,0,3,4,6,8,10,13,15] => Christopher[59] 72 | [Christopher,0,3,4,6,8,11,12,14] => Christopher[58] 73 | [Christopher,0,3,4,6,8,11,12,15] => Christopher[59] 74 | [Christopher,0,3,4,6,8,11,13,14] => Christopher[59] 75 | [Christopher,0,3,4,6,8,11,13,15] => Christopher[60] 76 | [Christopher,0,3,4,6,9,10,12,14] => Christopher[58] 77 | [Christopher,0,3,4,6,9,10,12,15] => Christopher[59] 78 | [Christopher,0,3,4,6,9,10,13,14] => Christopher[59] 79 | [Christopher,0,3,4,6,9,10,13,15] => Christopher[60] 80 | [Christopher,0,3,4,6,9,11,12,14] => Christopher[59] 81 | [Christopher,0,3,4,6,9,11,12,15] => Christopher[60] 82 | [Christopher,0,3,4,6,9,11,13,14] => Christopher[60] 83 | [Christopher,0,3,4,6,9,11,13,15] => Christopher[61] 84 | [Christopher,0,3,4,7,8,10,12,14] => Christopher[58] 85 | [Christopher,0,3,4,7,8,10,12,15] => Christopher[59] 86 | [Christopher,0,3,4,7,8,10,13,14] => Christopher[59] 87 | [Christopher,0,3,4,7,8,10,13,15] => Christopher[60] 88 | [Christopher,0,3,4,7,8,11,12,14] => Christopher[59] 89 | [Christopher,0,3,4,7,8,11,12,15] => Christopher[60] 90 | [Christopher,0,3,4,7,8,11,13,14] => Christopher[60] 91 | [Christopher,0,3,4,7,8,11,13,15] => Christopher[61] 92 | [Christopher,0,3,4,7,9,10,12,14] => Christopher[59] 93 | [Christopher,0,3,4,7,9,10,12,15] => Christopher[60] 94 | [Christopher,0,3,4,7,9,10,13,14] => Christopher[60] 95 | [Christopher,0,3,4,7,9,10,13,15] => Christopher[61] 96 | [Christopher,0,3,4,7,9,11,12,14] => Christopher[60] 97 | [Christopher,0,3,4,7,9,11,12,15] => Christopher[61] 98 | [Christopher,0,3,4,7,9,11,13,14] => Christopher[61] 99 | [Christopher,0,3,4,7,9,11,13,15] => Christopher[62] 100 | [Christopher,0,3,5,6,8,10,12,14] => Christopher[58] 101 | [Christopher,0,3,5,6,8,10,12,15] => Christopher[59] 102 | [Christopher,0,3,5,6,8,10,13,14] => Christopher[59] 103 | [Christopher,0,3,5,6,8,10,13,15] => Christopher[60] 104 | [Christopher,0,3,5,6,8,11,12,14] => Christopher[59] 105 | [Christopher,0,3,5,6,8,11,12,15] => Christopher[60] 106 | [Christopher,0,3,5,6,8,11,13,14] => Christopher[60] 107 | [Christopher,0,3,5,6,8,11,13,15] => Christopher[61] 108 | [Christopher,0,3,5,6,9,10,12,14] => Christopher[59] 109 | [Christopher,0,3,5,6,9,10,12,15] => Christopher[60] 110 | [Christopher,0,3,5,6,9,10,13,14] => Christopher[60] 111 | [Christopher,0,3,5,6,9,10,13,15] => Christopher[61] 112 | [Christopher,0,3,5,6,9,11,12,14] => Christopher[60] 113 | [Christopher,0,3,5,6,9,11,12,15] => Christopher[61] 114 | [Christopher,0,3,5,6,9,11,13,14] => Christopher[61] 115 | [Christopher,0,3,5,6,9,11,13,15] => Christopher[62] 116 | [Christopher,0,3,5,7,8,10,12,14] => Christopher[59] 117 | [Christopher,0,3,5,7,8,10,12,15] => Christopher[60] 118 | [Christopher,0,3,5,7,8,10,13,14] => Christopher[60] 119 | [Christopher,0,3,5,7,8,10,13,15] => Christopher[61] 120 | [Christopher,0,3,5,7,8,11,12,14] => Christopher[60] 121 | [Christopher,0,3,5,7,8,11,12,15] => Christopher[61] 122 | [Christopher,0,3,5,7,8,11,13,14] => Christopher[61] 123 | [Christopher,0,3,5,7,8,11,13,15] => Christopher[62] 124 | [Christopher,0,3,5,7,9,10,12,14] => Christopher[60] 125 | [Christopher,0,3,5,7,9,10,12,15] => Christopher[61] 126 | [Christopher,0,3,5,7,9,10,13,14] => Christopher[61] 127 | [Christopher,0,3,5,7,9,10,13,15] => Christopher[62] 128 | [Christopher,0,3,5,7,9,11,12,14] => Christopher[61] 129 | [Christopher,0,3,5,7,9,11,12,15] => Christopher[62] 130 | [Christopher,0,3,5,7,9,11,13,14] => Christopher[62] 131 | [Christopher,0,3,5,7,9,11,13,15] => Christopher[63] 132 | [Christopher,1,2,4,6,8,10,12,14] => Christopher[57] 133 | [Christopher,1,2,4,6,8,10,12,15] => Christopher[58] 134 | [Christopher,1,2,4,6,8,10,13,14] => Christopher[58] 135 | [Christopher,1,2,4,6,8,10,13,15] => Christopher[59] 136 | [Christopher,1,2,4,6,8,11,12,14] => Christopher[58] 137 | [Christopher,1,2,4,6,8,11,12,15] => Christopher[59] 138 | [Christopher,1,2,4,6,8,11,13,14] => Christopher[59] 139 | [Christopher,1,2,4,6,8,11,13,15] => Christopher[60] 140 | [Christopher,1,2,4,6,9,10,12,14] => Christopher[58] 141 | [Christopher,1,2,4,6,9,10,12,15] => Christopher[59] 142 | [Christopher,1,2,4,6,9,10,13,14] => Christopher[59] 143 | [Christopher,1,2,4,6,9,10,13,15] => Christopher[60] 144 | [Christopher,1,2,4,6,9,11,12,14] => Christopher[59] 145 | [Christopher,1,2,4,6,9,11,12,15] => Christopher[60] 146 | [Christopher,1,2,4,6,9,11,13,14] => Christopher[60] 147 | [Christopher,1,2,4,6,9,11,13,15] => Christopher[61] 148 | [Christopher,1,2,4,7,8,10,12,14] => Christopher[58] 149 | [Christopher,1,2,4,7,8,10,12,15] => Christopher[59] 150 | [Christopher,1,2,4,7,8,10,13,14] => Christopher[59] 151 | [Christopher,1,2,4,7,8,10,13,15] => Christopher[60] 152 | [Christopher,1,2,4,7,8,11,12,14] => Christopher[59] 153 | [Christopher,1,2,4,7,8,11,12,15] => Christopher[60] 154 | [Christopher,1,2,4,7,8,11,13,14] => Christopher[60] 155 | [Christopher,1,2,4,7,8,11,13,15] => Christopher[61] 156 | [Christopher,1,2,4,7,9,10,12,14] => Christopher[59] 157 | [Christopher,1,2,4,7,9,10,12,15] => Christopher[60] 158 | [Christopher,1,2,4,7,9,10,13,14] => Christopher[60] 159 | [Christopher,1,2,4,7,9,10,13,15] => Christopher[61] 160 | [Christopher,1,2,4,7,9,11,12,14] => Christopher[60] 161 | [Christopher,1,2,4,7,9,11,12,15] => Christopher[61] 162 | [Christopher,1,2,4,7,9,11,13,14] => Christopher[61] 163 | [Christopher,1,2,4,7,9,11,13,15] => Christopher[62] 164 | [Christopher,1,2,5,6,8,10,12,14] => Christopher[58] 165 | [Christopher,1,2,5,6,8,10,12,15] => Christopher[59] 166 | [Christopher,1,2,5,6,8,10,13,14] => Christopher[59] 167 | [Christopher,1,2,5,6,8,10,13,15] => Christopher[60] 168 | [Christopher,1,2,5,6,8,11,12,14] => Christopher[59] 169 | [Christopher,1,2,5,6,8,11,12,15] => Christopher[60] 170 | [Christopher,1,2,5,6,8,11,13,14] => Christopher[60] 171 | [Christopher,1,2,5,6,8,11,13,15] => Christopher[61] 172 | [Christopher,1,2,5,6,9,10,12,14] => Christopher[59] 173 | [Christopher,1,2,5,6,9,10,12,15] => Christopher[60] 174 | [Christopher,1,2,5,6,9,10,13,14] => Christopher[60] 175 | [Christopher,1,2,5,6,9,10,13,15] => Christopher[61] 176 | [Christopher,1,2,5,6,9,11,12,14] => Christopher[60] 177 | [Christopher,1,2,5,6,9,11,12,15] => Christopher[61] 178 | [Christopher,1,2,5,6,9,11,13,14] => Christopher[61] 179 | [Christopher,1,2,5,6,9,11,13,15] => Christopher[62] 180 | [Christopher,1,2,5,7,8,10,12,14] => Christopher[59] 181 | [Christopher,1,2,5,7,8,10,12,15] => Christopher[60] 182 | [Christopher,1,2,5,7,8,10,13,14] => Christopher[60] 183 | [Christopher,1,2,5,7,8,10,13,15] => Christopher[61] 184 | [Christopher,1,2,5,7,8,11,12,14] => Christopher[60] 185 | [Christopher,1,2,5,7,8,11,12,15] => Christopher[61] 186 | [Christopher,1,2,5,7,8,11,13,14] => Christopher[61] 187 | [Christopher,1,2,5,7,8,11,13,15] => Christopher[62] 188 | [Christopher,1,2,5,7,9,10,12,14] => Christopher[60] 189 | [Christopher,1,2,5,7,9,10,12,15] => Christopher[61] 190 | [Christopher,1,2,5,7,9,10,13,14] => Christopher[61] 191 | [Christopher,1,2,5,7,9,10,13,15] => Christopher[62] 192 | [Christopher,1,2,5,7,9,11,12,14] => Christopher[61] 193 | [Christopher,1,2,5,7,9,11,12,15] => Christopher[62] 194 | [Christopher,1,2,5,7,9,11,13,14] => Christopher[62] 195 | [Christopher,1,2,5,7,9,11,13,15] => Christopher[63] 196 | [Christopher,1,3,4,6,8,10,12,14] => Christopher[58] 197 | [Christopher,1,3,4,6,8,10,12,15] => Christopher[59] 198 | [Christopher,1,3,4,6,8,10,13,14] => Christopher[59] 199 | [Christopher,1,3,4,6,8,10,13,15] => Christopher[60] 200 | [Christopher,1,3,4,6,8,11,12,14] => Christopher[59] 201 | [Christopher,1,3,4,6,8,11,12,15] => Christopher[60] 202 | [Christopher,1,3,4,6,8,11,13,14] => Christopher[60] 203 | [Christopher,1,3,4,6,8,11,13,15] => Christopher[61] 204 | [Christopher,1,3,4,6,9,10,12,14] => Christopher[59] 205 | [Christopher,1,3,4,6,9,10,12,15] => Christopher[60] 206 | [Christopher,1,3,4,6,9,10,13,14] => Christopher[60] 207 | [Christopher,1,3,4,6,9,10,13,15] => Christopher[61] 208 | [Christopher,1,3,4,6,9,11,12,14] => Christopher[60] 209 | [Christopher,1,3,4,6,9,11,12,15] => Christopher[61] 210 | [Christopher,1,3,4,6,9,11,13,14] => Christopher[61] 211 | [Christopher,1,3,4,6,9,11,13,15] => Christopher[62] 212 | [Christopher,1,3,4,7,8,10,12,14] => Christopher[59] 213 | [Christopher,1,3,4,7,8,10,12,15] => Christopher[60] 214 | [Christopher,1,3,4,7,8,10,13,14] => Christopher[60] 215 | [Christopher,1,3,4,7,8,10,13,15] => Christopher[61] 216 | [Christopher,1,3,4,7,8,11,12,14] => Christopher[60] 217 | [Christopher,1,3,4,7,8,11,12,15] => Christopher[61] 218 | [Christopher,1,3,4,7,8,11,13,14] => Christopher[61] 219 | [Christopher,1,3,4,7,8,11,13,15] => Christopher[62] 220 | [Christopher,1,3,4,7,9,10,12,14] => Christopher[60] 221 | [Christopher,1,3,4,7,9,10,12,15] => Christopher[61] 222 | [Christopher,1,3,4,7,9,10,13,14] => Christopher[61] 223 | [Christopher,1,3,4,7,9,10,13,15] => Christopher[62] 224 | [Christopher,1,3,4,7,9,11,12,14] => Christopher[61] 225 | [Christopher,1,3,4,7,9,11,12,15] => Christopher[62] 226 | [Christopher,1,3,4,7,9,11,13,14] => Christopher[62] 227 | [Christopher,1,3,4,7,9,11,13,15] => Christopher[63] 228 | [Christopher,1,3,5,6,8,10,12,14] => Christopher[59] 229 | [Christopher,1,3,5,6,8,10,12,15] => Christopher[60] 230 | [Christopher,1,3,5,6,8,10,13,14] => Christopher[60] 231 | [Christopher,1,3,5,6,8,10,13,15] => Christopher[61] 232 | [Christopher,1,3,5,6,8,11,12,14] => Christopher[60] 233 | [Christopher,1,3,5,6,8,11,12,15] => Christopher[61] 234 | [Christopher,1,3,5,6,8,11,13,14] => Christopher[61] 235 | [Christopher,1,3,5,6,8,11,13,15] => Christopher[62] 236 | [Christopher,1,3,5,6,9,10,12,14] => Christopher[60] 237 | [Christopher,1,3,5,6,9,10,12,15] => Christopher[61] 238 | [Christopher,1,3,5,6,9,10,13,14] => Christopher[61] 239 | [Christopher,1,3,5,6,9,10,13,15] => Christopher[62] 240 | [Christopher,1,3,5,6,9,11,12,14] => Christopher[61] 241 | [Christopher,1,3,5,6,9,11,12,15] => Christopher[62] 242 | [Christopher,1,3,5,6,9,11,13,14] => Christopher[62] 243 | [Christopher,1,3,5,6,9,11,13,15] => Christopher[63] 244 | [Christopher,1,3,5,7,8,10,12,14] => Christopher[60] 245 | [Christopher,1,3,5,7,8,10,12,15] => Christopher[61] 246 | [Christopher,1,3,5,7,8,10,13,14] => Christopher[61] 247 | [Christopher,1,3,5,7,8,10,13,15] => Christopher[62] 248 | [Christopher,1,3,5,7,8,11,12,14] => Christopher[61] 249 | [Christopher,1,3,5,7,8,11,12,15] => Christopher[62] 250 | [Christopher,1,3,5,7,8,11,13,14] => Christopher[62] 251 | [Christopher,1,3,5,7,8,11,13,15] => Christopher[63] 252 | [Christopher,1,3,5,7,9,10,12,14] => Christopher[61] 253 | [Christopher,1,3,5,7,9,10,12,15] => Christopher[62] 254 | [Christopher,1,3,5,7,9,10,13,14] => Christopher[62] 255 | [Christopher,1,3,5,7,9,10,13,15] => Christopher[63] 256 | [Christopher,1,3,5,7,9,11,12,14] => Christopher[62] 257 | [Christopher,1,3,5,7,9,11,12,15] => Christopher[63] 258 | [Christopher,1,3,5,7,9,11,13,14] => Christopher[63] 259 | [Christopher,1,3,5,7,9,11,13,15] => Christopher[64] --------------------------------------------------------------------------------