docker-compose/cmd/compose/options_test.go

395 lines
10 KiB
Go
Raw Permalink Normal View History

build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/mocks"
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
)
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Services: types.Services{
"test": {
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
"alice/32",
},
},
Platform: "alice/32",
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
project.Services["test"].Build.Platforms)
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
})
}
func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
},
Services: types.Services{
"test": {
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
project.Services["test"].Build.Platforms)
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
})
}
func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
},
Services: types.Services{
"test": {
build: pass BuildOptions around explicitly & fix multi-platform issues The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 08:47:09 -04:00
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, true),
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, false),
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
}
func TestIsRemoteConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
configPaths []string
want bool
}{
{
name: "empty config paths",
configPaths: []string{},
want: false,
},
{
name: "local file",
configPaths: []string{"docker-compose.yaml"},
want: false,
},
{
name: "OCI reference",
configPaths: []string{"oci://registry.example.com/stack:latest"},
want: true,
},
{
name: "GIT reference",
configPaths: []string{"git://github.com/user/repo.git"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: tt.configPaths,
},
}
got := isRemoteConfig(cli, opts)
require.Equal(t, tt.want, got)
})
}
}
func TestDisplayLocationRemoteStack(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
buf := new(bytes.Buffer)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
project := &types.Project{
Name: "test-project",
WorkingDir: "/tmp/test",
}
options := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
},
}
displayLocationRemoteStack(cli, project, options)
output := buf.String()
require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
}
func TestDisplayInterpolationVariables(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "compose-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a temporary compose file
composeContent := `
services:
app:
image: nginx
environment:
- TEST_VAR=${TEST_VAR:?required} # required with default
- API_KEY=${API_KEY:?} # required without default
- DEBUG=${DEBUG:-true} # optional with default
- UNSET_VAR # optional without default
`
composePath := filepath.Join(tmpDir, "docker-compose.yml")
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
require.NoError(t, err)
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
// Create ProjectOptions with the temporary compose file
projectOptions := &ProjectOptions{
ConfigPaths: []string{composePath},
}
// Set up the context with necessary environment variables
ctx := context.Background()
_ = os.Setenv("TEST_VAR", "test-value")
_ = os.Setenv("API_KEY", "123456")
defer func() {
_ = os.Unsetenv("TEST_VAR")
_ = os.Unsetenv("API_KEY")
}()
// Extract variables from the model
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
require.NoError(t, err)
require.False(t, noVariables)
// Display the variables
displayInterpolationVariables(cli.Out(), info)
// Expected output format with proper spacing
expected := "\nFound the following variables in configuration:\n" +
"VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
"API_KEY 123456 environment yes \n" +
"DEBUG true compose file no true\n" +
"TEST_VAR test-value environment yes \n"
// Normalize spaces and newlines for comparison
normalizeSpaces := func(s string) string {
// Replace multiple spaces with a single space
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
return s
}
actualOutput := buf.String()
// Compare normalized strings
require.Equal(t,
normalizeSpaces(expected),
normalizeSpaces(actualOutput),
"\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
}
func TestConfirmRemoteIncludes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
opts buildOptions
assumeYes bool
userInput string
wantErr bool
errMessage string
wantPrompt bool
wantOutput string
}{
{
name: "no remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"docker-compose.yaml",
"./local/path/compose.yaml",
},
},
},
assumeYes: false,
wantErr: false,
wantPrompt: false,
},
{
name: "assume yes with remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: true,
wantErr: false,
wantPrompt: false,
},
{
name: "user confirms remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: false,
userInput: "y\n",
wantErr: false,
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
" - git://github.com/user/repo.git\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
{
name: "user rejects remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
},
},
},
assumeYes: false,
userInput: "n\n",
wantErr: true,
errMessage: "operation cancelled by user",
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
}
buf := new(bytes.Buffer)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
if tt.wantPrompt {
inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
}
err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
if tt.wantErr {
require.Error(t, err)
require.Equal(t, tt.errMessage, err.Error())
} else {
require.NoError(t, err)
}
if tt.wantOutput != "" {
require.Equal(t, tt.wantOutput, buf.String())
}
buf.Reset()
})
}
}