Repository: uber-go/cff (language: go)

Issue:
Using cff in test files
'cff' currently suffixes generated files with `_gen`.
This means we cannot use cff in test files because they'll become foo_test_gen.go.
We should special case test files:
if the file name ends with _test.go, the generated file should be foo_gen_test.go.



Discussion / hints:

Fix patch:
diff --git a/cmd/cff/main.go b/cmd/cff/main.go
index 51484df..2bdb5ab 100644
--- a/cmd/cff/main.go
+++ b/cmd/cff/main.go
@@ -125,12 +125,7 @@ func run(args []string) error {
 			}
 
 			if len(output) == 0 {
-				// x/y/foo.go => x/y/foo_gen.go
-				output = filepath.Join(
-					filepath.Dir(path),
-					// foo.go => foo + _gen.go
-					strings.TrimSuffix(name, filepath.Ext(name))+"_gen.go",
-				)
+				output = genFilename(path)
 			}
 
 			processed++
@@ -146,3 +141,22 @@ func run(args []string) error {
 	}
 	return err
 }
+
+// genFilename returns the default file name for the generated output file.
+func genFilename(path string) string {
+	name := filepath.Base(path)
+	var genname string
+	if strings.HasSuffix(name, "_test.go") {
+		// foo_test.go => foo + _gen_test.go
+		genname = strings.TrimSuffix(name, "_test.go") + "_gen_test.go"
+	} else {
+		// foo.go => foo + _gen.go
+		genname = strings.TrimSuffix(name, filepath.Ext(name)) + "_gen.go"
+	}
+	// x/y/foo.go => x/y/foo_gen.go
+	// x/y/foo_test.go => x/y/foo_gen_test.go
+	return filepath.Join(
+		filepath.Dir(path),
+		genname,
+	)
+}
diff --git a/internal/pkg/gopackages.go b/internal/pkg/gopackages.go
index a523917..5f67d67 100644
--- a/internal/pkg/gopackages.go
+++ b/internal/pkg/gopackages.go
@@ -78,6 +78,7 @@ func (l *goPackagesLoader) Load(fset *token.FileSet, importPath string) ([]*Pack
 		Fset:       fset,
 		BuildFlags: []string{"-tags", strings.Join(uniqueTags, ",")},
 		Dir:        l.dir,
+		Tests:      true,
 	}, importPath)
 	if err != nil {
 		return nil, fmt.Errorf("packages.Load: %w", err)


Test patch:
diff --git a/cmd/cff/main_test.go b/cmd/cff/main_test.go
index 75c39a2..79e9994 100644
--- a/cmd/cff/main_test.go
+++ b/cmd/cff/main_test.go
@@ -93,3 +93,38 @@ func TestMain_ErrorNoPanic(t *testing.T) {
 		assert.Error(t, err)
 	})
 }
+
+func TestGenFilename(t *testing.T) {
+	tests := []struct {
+		desc    string
+		give    string
+		want    string
+	}{
+		{
+			desc: "source file",
+			give: "x/y/foo.go",
+			want: "x/y/foo_gen.go",
+		},
+		{
+			desc: "test file",
+			give: "x/y/foo_test.go",
+			want: "x/y/foo_gen_test.go",
+		},
+		{
+			desc: "source file abs path",
+			give: "/x/y/foo.go",
+			want: "/x/y/foo_gen.go",
+		},
+		{
+			desc: "test file abs path",
+			give: "/x/y/foo_test.go",
+			want: "/x/y/foo_gen_test.go",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			assert.Equal(t, tt.want, genFilename(tt.give))
+		})
+	}
+}
diff --git a/internal/tests/.gitattributes b/internal/tests/.gitattributes
index 5776675..89dd932 100644
--- a/internal/tests/.gitattributes
+++ b/internal/tests/.gitattributes
@@ -1,2 +1,3 @@
 # Mark cff-generated files generated.
 **/*_gen.go linguist-generated=true
+**/*_gen_test.go linguist-generated=true
diff --git a/internal/tests/cffintest/cffintest.go b/internal/tests/cffintest/cffintest.go
new file mode 100644
index 0000000..bc45f37
--- /dev/null
+++ b/internal/tests/cffintest/cffintest.go
@@ -0,0 +1,5 @@
+package cffintest
+
+func isOdd(n int) bool {
+	return n % 2 == 1
+}
diff --git a/internal/tests/cffintest/cffintest_gen_test.go b/internal/tests/cffintest/cffintest_gen_test.go
new file mode 100644
index 0000000..5f5fb08
--- /dev/null
+++ b/internal/tests/cffintest/cffintest_gen_test.go
@@ -0,0 +1,270 @@
+//go:build !cff
+// +build !cff
+
+package cffintest
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/cff"
+)
+
+func TestIsOdd(t *testing.T) {
+	err := func() (err error) {
+
+		_17_3 := context.Background()
+
+		_18_19 := 4
+
+		_20_4 := func() error {
+			assert.Equal(t, true, isOdd(1))
+			return nil
+		}
+
+		_24_4 := func() error {
+			assert.Equal(t, false, isOdd(2))
+			return nil
+		}
+
+		_28_4 := func() error {
+			assert.Equal(t, true, isOdd(3))
+			return nil
+		}
+
+		_32_4 := func() error {
+			assert.Equal(t, false, isOdd(4))
+			return nil
+		}
+		ctx := _17_3
+		emitter := cff.NopEmitter()
+
+		var (
+			parallelInfo = &cff.ParallelInfo{
+				File:   "go.uber.org/cff/internal/tests/cffintest/cffintest_test.go",
+				Line:   16,
+				Column: 9,
+			}
+			directiveInfo = &cff.DirectiveInfo{
+				Name:      parallelInfo.Name,
+				Directive: cff.ParallelDirective,
+				File:      parallelInfo.File,
+				Line:      parallelInfo.Line,
+				Column:    parallelInfo.Column,
+			}
+			parallelEmitter = cff.NopParallelEmitter()
+
+			schedInfo = &cff.SchedulerInfo{
+				Name:      parallelInfo.Name,
+				Directive: cff.ParallelDirective,
+				File:      parallelInfo.File,
+				Line:      parallelInfo.Line,
+				Column:    parallelInfo.Column,
+			}
+
+			// possibly unused
+			_ = parallelInfo
+			_ = directiveInfo
+		)
+
+		startTime := time.Now()
+		defer func() { parallelEmitter.ParallelDone(ctx, time.Since(startTime)) }()
+
+		schedEmitter := emitter.SchedulerInit(schedInfo)
+
+		sched := cff.NewScheduler(
+			cff.SchedulerParams{
+				Concurrency: _18_19, Emitter: schedEmitter,
+			},
+		)
+
+		var tasks []*struct {
+			emitter cff.TaskEmitter
+			fn      func(context.Context) error
+			ran     cff.AtomicBool
+		}
+		defer func() {
+			for _, t := range tasks {
+				if !t.ran.Load() {
+					t.emitter.TaskSkipped(ctx, err)
+				}
+			}
+		}()
+
+		// go.uber.org/cff/internal/tests/cffintest/cffintest_test.go:20:4
+		task0 := new(struct {
+			emitter cff.TaskEmitter
+			fn      func(context.Context) error
+			ran     cff.AtomicBool
+		})
+		task0.emitter = cff.NopTaskEmitter()
+		task0.fn = func(ctx context.Context) (err error) {
+			taskEmitter := task0.emitter
+			startTime := time.Now()
+			defer func() {
+				if task0.ran.Load() {
+					taskEmitter.TaskDone(ctx, time.Since(startTime))
+				}
+			}()
+
+			defer func() {
+				recovered := recover()
+				if recovered != nil {
+					taskEmitter.TaskPanic(ctx, recovered)
+					err = fmt.Errorf("panic: %v", recovered)
+				}
+			}()
+
+			defer task0.ran.Store(true)
+
+			err = _20_4()
+
+			if err != nil {
+				taskEmitter.TaskError(ctx, err)
+				return
+			}
+			taskEmitter.TaskSuccess(ctx)
+			return
+		}
+
+		sched.Enqueue(ctx, cff.Job{
+			Run: task0.fn,
+		})
+		tasks = append(tasks, task0)
+
+		// go.uber.org/cff/internal/tests/cffintest/cffintest_test.go:24:4
+		task1 := new(struct {
+			emitter cff.TaskEmitter
+			fn      func(context.Context) error
+			ran     cff.AtomicBool
+		})
+		task1.emitter = cff.NopTaskEmitter()
+		task1.fn = func(ctx context.Context) (err error) {
+			taskEmitter := task1.emitter
+			startTime := time.Now()
+			defer func() {
+				if task1.ran.Load() {
+					taskEmitter.TaskDone(ctx, time.Since(startTime))
+				}
+			}()
+
+			defer func() {
+				recovered := recover()
+				if recovered != nil {
+					taskEmitter.TaskPanic(ctx, recovered)
+					err = fmt.Errorf("panic: %v", recovered)
+				}
+			}()
+
+			defer task1.ran.Store(true)
+
+			err = _24_4()
+
+			if err != nil {
+				taskEmitter.TaskError(ctx, err)
+				return
+			}
+			taskEmitter.TaskSuccess(ctx)
+			return
+		}
+
+		sched.Enqueue(ctx, cff.Job{
+			Run: task1.fn,
+		})
+		tasks = append(tasks, task1)
+
+		// go.uber.org/cff/internal/tests/cffintest/cffintest_test.go:28:4
+		task2 := new(struct {
+			emitter cff.TaskEmitter
+			fn      func(context.Context) error
+			ran     cff.AtomicBool
+		})
+		task2.emitter = cff.NopTaskEmitter()
+		task2.fn = func(ctx context.Context) (err error) {
+			taskEmitter := task2.emitter
+			startTime := time.Now()
+			defer func() {
+				if task2.ran.Load() {
+					taskEmitter.TaskDone(ctx, time.Since(startTime))
+				}
+			}()
+
+			defer func() {
+				recovered := recover()
+				if recovered != nil {
+					taskEmitter.TaskPanic(ctx, recovered)
+					err = fmt.Errorf("panic: %v", recovered)
+				}
+			}()
+
+			defer task2.ran.Store(true)
+
+			err = _28_4()
+
+			if err != nil {
+				taskEmitter.TaskError(ctx, err)
+				return
+			}
+			taskEmitter.TaskSuccess(ctx)
+			return
+		}
+
+		sched.Enqueue(ctx, cff.Job{
+			Run: task2.fn,
+		})
+		tasks = append(tasks, task2)
+
+		// go.uber.org/cff/internal/tests/cffintest/cffintest_test.go:32:4
+		task3 := new(struct {
+			emitter cff.TaskEmitter
+			fn      func(context.Context) error
+			ran     cff.AtomicBool
+		})
+		task3.emitter = cff.NopTaskEmitter()
+		task3.fn = func(ctx context.Context) (err error) {
+			taskEmitter := task3.emitter
+			startTime := time.Now()
+			defer func() {
+				if task3.ran.Load() {
+					taskEmitter.TaskDone(ctx, time.Since(startTime))
+				}
+			}()
+
+			defer func() {
+				recovered := recover()
+				if recovered != nil {
+					taskEmitter.TaskPanic(ctx, recovered)
+					err = fmt.Errorf("panic: %v", recovered)
+				}
+			}()
+
+			defer task3.ran.Store(true)
+
+			err = _32_4()
+
+			if err != nil {
+				taskEmitter.TaskError(ctx, err)
+				return
+			}
+			taskEmitter.TaskSuccess(ctx)
+			return
+		}
+
+		sched.Enqueue(ctx, cff.Job{
+			Run: task3.fn,
+		})
+		tasks = append(tasks, task3)
+
+		if err := sched.Wait(ctx); err != nil {
+			parallelEmitter.ParallelError(ctx, err)
+			return err
+		}
+		parallelEmitter.ParallelSuccess(ctx)
+		return nil /*line cffintest_test.go:36*/
+	}()
+	require.NoError(t, err)
+}
diff --git a/internal/tests/cffintest/cffintest_test.go b/internal/tests/cffintest/cffintest_test.go
new file mode 100644
index 0000000..13a0fcd
--- /dev/null
+++ b/internal/tests/cffintest/cffintest_test.go
@@ -0,0 +1,39 @@
+//go:build cff
+// +build cff
+
+package cffintest
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/cff"
+)
+
+func TestIsOdd(t *testing.T) {
+	err := cff.Parallel(
+		context.Background(),
+		cff.Concurrency(4),
+		cff.Tasks(
+			func() error {
+				assert.Equal(t, true, isOdd(1))
+				return nil
+			},
+			func() error {
+				assert.Equal(t, false, isOdd(2))
+				return nil
+			},
+			func() error {
+				assert.Equal(t, true, isOdd(3))
+				return nil
+			},
+			func() error {
+				assert.Equal(t, false, isOdd(4))
+				return nil
+			},
+		),
+	)
+	require.NoError(t, err)
+}