Repository: google/yamlfmt
Pull request #221 (2024-11-22 09:55:07)

Document and improve integration test case creation.
This does two things:

1. The creation of new test cases is simplified by generating all necessary files and a stub test case in `command_test.go`. Previously only the directories were created. With this change, `TESTNAME=my_new_test make command_test_case integrationtest` will succeed, i.e. the generated test case passes.
2. Integration tests and their maintenance functions are documented.


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b41a1f0..cbb1ce1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -21,12 +21,12 @@ jobs:
   run-lint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Set up Go
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
-          go-version: 1.18
+          go-version: 1.23
           cache: true
 
       - name: Install tools
@@ -39,12 +39,12 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: [ '1.18', '1.19', '1.20', '1.21', '1.22' ]
+        go: [ '1.21', '1.22', '1.23' ]
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Set up Go
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ matrix.go }}
           cache: true
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index ff23af5..30f9d88 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -29,13 +29,13 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/')
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Setup Golang
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
-          go-version: 1.18
+          go-version: 1.23
           check-latest: true
           cache: true
       - name: Install Cosign
diff --git a/Makefile b/Makefile
index 314a11e..2790ae3 100644
--- a/Makefile
+++ b/Makefile
@@ -43,8 +43,7 @@ command_test_case:
 ifndef TESTNAME
 	$(error "TESTNAME undefined")
 endif
-	mkdir -p integrationtest/command/testdata/$(TESTNAME)/before && \
-	mkdir -p integrationtest/command/testdata/$(TESTNAME)/stdout
+	./integrationtest/command/new_test_case.sh "$(TESTNAME)"
 
 .PHONY: install
 install:
diff --git a/README.md b/README.md
index 4c31bcb..497e82a 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ To download the `yamlfmt` command, you can download the desired binary from rele
 ```
 go install github.com/google/yamlfmt/cmd/yamlfmt@latest
 ```
-This currently requires Go version 1.18 or greater.
+This currently requires Go version 1.21 or greater.
 
 NOTE: Recommended setup if this is your first time installing Go would be in [this DigitalOcean blog post](https://www.digitalocean.com/community/tutorials/how-to-build-and-install-go-programs).
 
diff --git a/docs/paths.md b/docs/paths.md
index 94330eb..a98236c 100644
--- a/docs/paths.md
+++ b/docs/paths.md
@@ -6,10 +6,36 @@
 
 In standard path mode, you can specify a file or directory path directly. If specifying a file, it will simply include the file. If specifying a directory, it will include every file with the correct extension (as specified in `extensions`, default is `yml` and `yaml`).
 
+This mode does *not* support wildcards, aka. globbing. That means with `*.yaml` yamlfmt will look for a file named asterisk dot yaml. If you require globbing, use the [Doublestar mode](#doublestar) instead.
+
 ## Doublestar
 
-In Doublestar mode, paths are specified using the format explained in the [doublestar](https://github.com/bmatcuk/doublestar) package. It is almost identical to bash and git's style of glob pattern specification.
+In Doublestar mode, paths are specified using wildcard patterns explained in the [doublestar](https://github.com/bmatcuk/doublestar) package. It is almost identical to bash and git's style of glob pattern specification.
+
+To enable the doublestar mode, set `doublestar: true` in the config file or use the `-dstar` command line flag.
 
 ## Include and Exclude
 
 In both modes, `yamlfmt` will allow you to configure include and exclude paths. These can be paths to files in Standard or Doublestar modes, paths to directories in Standard mode, and valid doublestar patterns in Doublestar mode. These paths should be specified **relative to the working directory of `yamlfmt`**. They will work as absolute paths if both the includes and excludes are specified as absolute paths or if both are relative paths, however it will not work as expected if they are mixed together. It usually easier to reason about includes and excludes when always specifying both as relative paths from the directory `yamlfmt` is going to be run in.
+
+Exclude paths can be specified on the command line using the `-exclude` flag.
+Paths excluded from the command line are **added* to excluded paths from the config file.
+
+Include paths can be specified on the command line via the positional arguments, i.e. there is no flag for it.
+Paths from the command line take precedence over and **replace** any paths configured in the config file.
+
+yamlfmt will build a list of all files to format using the include list, then discard any files matching the exclude list.
+
+## Extensions
+
+*Only in standard mode*
+
+By default, yamlfmt formats all files ending in `.yaml` and `.yml`.
+You can modify this behavior using the config file and command line flags.
+
+The config file **sets** the list of extensions.
+For example, with `extensions: ["foo"]`, yamlfmt will only match files ending in `.foo` and will *not* match files ending in `.yaml` or `.yml`.
+An empty list triggers the default behavior.
+
+The `-extensions` command line flag **adds** to the list of extensions from the config file.
+For example, `-extensions yaml.gotmpl` will match files ending in `.yaml.gotmpl` *in addition to* files ending in `.yaml` and `.yml`.
diff --git a/go.mod b/go.mod
index f7df79f..b710e29 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,14 @@
 module github.com/google/yamlfmt
 
-go 1.18
+go 1.21
 
 require (
-	github.com/bmatcuk/doublestar/v4 v4.6.0
+	github.com/bmatcuk/doublestar/v4 v4.7.1
 	github.com/braydonk/yaml v0.7.0
-	github.com/google/go-cmp v0.5.9
+	github.com/google/go-cmp v0.6.0
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
 )
 
-require github.com/santhosh-tekuri/jsonschema/v4 v4.0.0
+require golang.org/x/text v0.14.0 // indirect
diff --git a/go.sum b/go.sum
index 3a691f0..95ce3e6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,22 +1,26 @@
-github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
-github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
+github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/braydonk/yaml v0.7.0 h1:ySkqO7r0MGoCNhiRJqE0Xe9yhINMyvOAB3nFjgyJn2k=
 github.com/braydonk/yaml v0.7.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
-github.com/santhosh-tekuri/jsonschema/v4 v4.0.0 h1:5yvbsmEfOQE2+YuRudUhXPzMQK6P74AR3LafM5zrzf0=
-github.com/santhosh-tekuri/jsonschema/v4 v4.0.0/go.mod h1:P2UNYJ2xnL4zRyQ2obmD35HiejSJhHTKwMcckhPucG4=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
diff --git a/path_collector.go b/path_collector.go
index 7b7fbcc..2b8d516 100644
--- a/path_collector.go
+++ b/path_collector.go
@@ -95,18 +95,13 @@ func (c *FilepathCollector) CollectPaths() ([]string, error) {
 }
 
 func (c *FilepathCollector) walkDirectoryForYaml(dir string) ([]string, error) {
-	paths := []string{}
+	var paths []string
 	err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
 		if info.IsDir() {
 			return nil
 		}
 
-		extension := ""
-		if strings.Contains(info.Name(), ".") {
-			nameParts := strings.Split(info.Name(), ".")
-			extension = nameParts[len(nameParts)-1]
-		}
-		if collections.SliceContains(c.Extensions, extension) {
+		if c.extensionMatches(info.Name()) {
 			paths = append(paths, path)
 		}
 
@@ -115,6 +110,20 @@ func (c *FilepathCollector) walkDirectoryForYaml(dir string) ([]string, error) {
 	return paths, err
 }
 
+func (c *FilepathCollector) extensionMatches(name string) bool {
+	for _, ext := range c.Extensions {
+		// Users may specify "yaml", but we only want to match ".yaml", not "buyaml".
+		if !strings.HasPrefix(ext, ".") {
+			ext = "." + ext
+		}
+
+		if strings.HasSuffix(name, ext) {
+			return true
+		}
+	}
+	return false
+}
+
 type DoublestarCollector struct {
 	Include []string
 	Exclude []string


Test patch:
diff --git a/docs/integration-test.md b/docs/integration-test.md
new file mode 100644
index 0000000..ba845ce
--- /dev/null
+++ b/docs/integration-test.md
@@ -0,0 +1,36 @@
+# Integration Testing
+
+yamlfmt has integration testing that tests invocations end-to-end.
+
+## Run
+
+Integration tests are excluded from `go test` by default using build tags. To build and execute the integration test, use:
+
+```shell
+make integrationtest
+```
+
+## Update
+
+The "golden" / expected output files can be (re-)generated by the test framework. To do so, run:
+
+```shell
+make integrationtest_update
+```
+
+## Add new test case
+
+* Run the following command to create the directory structure for the new test:
+
+  ```shell
+  TESTNAME=my_new_test make command_test_case
+  ```
+* Add example input files to `testdata/${TESTNAME}/before`.
+* Add the expected output to `testdata/${TESTNAME}/stdout/stdout.txt` if applicable.
+* `make command_test_case` added a new Go test to `integrationtest/command/command_test.go`. Update the `yamlfmt` flags so the invocation uses the feature the test should cover.
+* Run the following command to generate the expected "golden" output of the test:
+
+  ```shell
+  make integrationtest_update
+  ```
+* Review the generated output to ensure it is expected.
diff --git a/integrationtest/command/new_test_case.sh b/integrationtest/command/new_test_case.sh
new file mode 100755
index 0000000..cf97add
--- /dev/null
+++ b/integrationtest/command/new_test_case.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+
+# Copyright 2024 GitLab, Inc.
+#
+# 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.
+
+set -eu -o pipefail
+
+if [[ $# -ne 1 ]]; then
+	echo "Usage: $0 <testname>" >&2
+	exit 1
+fi
+
+TESTNAME="$1"
+shift
+
+declare -r BEFORE_DIR="integrationtest/command/testdata/${TESTNAME:?}/before"
+declare -r AFTER_DIR="integrationtest/command/testdata/${TESTNAME:?}/after"
+declare -r OUTPUT_DIR="integrationtest/command/testdata/${TESTNAME:?}/stdout"
+declare -r GO_TEST_FILE="integrationtest/command/command_test.go"
+
+if [[ ! -d "${BEFORE_DIR}" ]]; then
+	mkdir -p "${BEFORE_DIR}"
+	cat >"${BEFORE_DIR}/x.yaml" <<EOF
+TODO: add test input
+EOF
+fi
+
+if [[ ! -d "${AFTER_DIR}" ]]; then
+	mkdir -p "${AFTER_DIR}"
+	cat >"${AFTER_DIR}/x.yaml" <<EOF
+TODO: add test input
+EOF
+fi
+
+mkdir -p "${OUTPUT_DIR}"
+touch "${OUTPUT_DIR}/stdout.txt"
+touch "${OUTPUT_DIR}/stderr.txt"
+
+# Converty "my_test_case" to "MyTestCase".
+if sed --version 2>/dev/null | grep -q GNU; then
+	# \U (convert to upper case) is a GNU extension.
+	GO_TESTNAME="$(sed -r -e 's/(^|_)(\w)/\U\2/g' <<<"${TESTNAME}")"
+else
+	# Fall back to Perl.
+	GO_TESTNAME="$(perl -pe 's/(^|_)(\w)/\U\2/g' <<<"${TESTNAME}")"
+fi
+
+if ! grep -q -F "Test${GO_TESTNAME:?}" "${GO_TEST_FILE}"; then
+	cat >>"${GO_TEST_FILE}" <<EOF
+
+func Test${GO_TESTNAME:?}(t *testing.T) {
+	TestCase{
+		Dir:     "${TESTNAME:?}",
+		// TODO: Change arguments to match your test case.
+		Command: yamlfmtWithArgs("."),
+		Update:  *updateFlag,
+	}.Run(t)
+}
+EOF
+	gofmt -w "${GO_TEST_FILE}"
+fi
+
+cat <<EOF
+Next steps:
+
+* Write test input files to '${BEFORE_DIR}'
+* Write expected output to '${OUTPUT_DIR}/stdout.txt'
+* Optional: write expected error output to '${OUTPUT_DIR}/stderr.txt'
+* Add yamlfmt arguments to the 'Test${GO_TESTNAME}' test in '${GO_TEST_FILE}'
+* Run 'make integrationtest_update' and review the files in '${AFTER_DIR}'
+EOF
diff --git a/internal/jsonschema/schema_test.go b/internal/jsonschema/schema_test.go
index 59eb453..5fcbd15 100644
--- a/internal/jsonschema/schema_test.go
+++ b/internal/jsonschema/schema_test.go
@@ -17,7 +17,7 @@ package jsonschema_test
 import (
 	"testing"
 
-	"github.com/santhosh-tekuri/jsonschema/v4"
+	"github.com/santhosh-tekuri/jsonschema/v6"
 )
 
 func TestJSONSchemaIsValid(t *testing.T) {
diff --git a/path_collector_test.go b/path_collector_test.go
index 9d6d90f..0ccf06e 100644
--- a/path_collector_test.go
+++ b/path_collector_test.go
@@ -193,6 +193,26 @@ func TestFilepathCollector(t *testing.T) {
 				"y.yaml": {},
 			},
 		},
+		{
+			name: "multi-part extension",
+			files: []tempfile.Path{
+				{FileName: "x.yaml"},
+				{FileName: "y.yaml.gotmpl"},
+				{FileName: "z.json"},
+			},
+			includePatterns: testPatterns{
+				{pattern: ""}, // with the test this functionally means the whole temp dir
+			},
+			extensions: []string{
+				"yaml",
+				"yml",
+				"yaml.gotmpl",
+			},
+			expectedFiles: collections.Set[string]{
+				"x.yaml":        {},
+				"y.yaml.gotmpl": {},
+			},
+		},
 	}.runAll(t, useFilepathCollector)
 }