├── 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 | [](https://godoc.org/github.com/approvals/go-approval-tests)
6 | [](https://goreportcard.com/report/github.com/approvals/go-approval-tests)
7 | [](https://codecov.io/gh/approvals/go-approval-tests)
8 | [](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]
--------------------------------------------------------------------------------