Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions run.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
package testy

import (
"errors"
"fmt"
"runtime/debug"
"sync"
"testing"
"time"
)

// ErrPackageNotFound indicates a requested registered package does not exist.
var ErrPackageNotFound = errors.New("package not found")

// RunOptions controls how the non-go-test runner executes registered tests.
type RunOptions struct {
// PackageConcurrency is the maximum number of packages executed at once.
// Values <= 1 preserve the historical serial package execution behavior.
PackageConcurrency int
}

// PackageSpec describes a package registered with testy.
type PackageSpec struct {
// Name is the package import path.
Name string
// Index is the package's deterministic registration-order index.
Index int
}

// RunAsTest runs all registered tests under Go's testing framework.
//
// To run tests on a per-package basis, put a test file in each package containing a single test that calls this function.
Expand Down Expand Up @@ -102,13 +114,9 @@ func Run() TestResult {
// RunWithOptions runs all registered tests and returns result information about them.
func RunWithOptions(opts RunOptions) TestResult {
start := time.Now()
results := TestResult{
Name: "Test Suite",
Started: start,
}

pkgs := collectPackages()
results.Subtests = make([]TestResult, len(pkgs))
packageResults := make([]TestResult, len(pkgs))

packageConcurrency := opts.PackageConcurrency
if packageConcurrency < 1 {
Expand All @@ -117,14 +125,52 @@ func RunWithOptions(opts RunOptions) TestResult {

if packageConcurrency == 1 {
for i := range pkgs {
results.Subtests[i] = runPackage(pkgs[i].name, pkgs[i].tests)
packageResults[i] = runPackage(pkgs[i].name, pkgs[i].tests)
}
} else {
runPackagesConcurrently(pkgs, results.Subtests, packageConcurrency)
runPackagesConcurrently(pkgs, packageResults, packageConcurrency)
}

return BuildSuiteResult(start, packageResults)
}

// ListPackages returns the registered packages in deterministic execution order.
//
// The returned package list is intentionally independent from any execution
// backend. Callers that want to orchestrate package execution outside this
// process, such as a durable workflow engine, can use the package names with
// RunPackage and then combine the package results with BuildSuiteResult.
func ListPackages() []PackageSpec {
pkgs := collectPackages()
specs := make([]PackageSpec, len(pkgs))
for i, pkg := range pkgs {
specs[i] = PackageSpec{
Name: pkg.name,
Index: i,
}
}
return specs
}

// RunPackage runs a single registered package by import path.
func RunPackage(pkg string) (TestResult, error) {
pkgTests, ok := instance.tests[pkg]
if !ok {
return TestResult{}, fmt.Errorf("%w: %s", ErrPackageNotFound, pkg)
}
return runPackage(pkg, pkgTests), nil
}

// BuildSuiteResult combines package-level results into a suite-level result.
func BuildSuiteResult(start time.Time, packageResults []TestResult) TestResult {
results := TestResult{
Name: "Test Suite",
Started: start,
Subtests: packageResults,
}

r := ResultPassed
for _, pkgResult := range results.Subtests {
for _, pkgResult := range packageResults {
if pkgResult.Result == ResultFailed {
r = ResultFailed
break
Expand Down
55 changes: 55 additions & 0 deletions run_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package testy

import (
"errors"
"fmt"
"sync/atomic"
"testing"
Expand All @@ -21,6 +22,60 @@ func succeeds(ts *time.Time) Tester {
*ts = time.Now()
}
}

func TestPackageRunnerAPI(t *testing.T) {
instance = testy{
tests: orderedmap.OrderedMap[string, *testPkg]{},
}
defer func() {
instance = testy{}
}()

for i := 0; i < 2; i++ {
pkg := fmt.Sprintf("github.com/gametimesf/testy/packageapi/pkg%d", i)
testName := fmt.Sprintf("test%d", i)
instance.tests[pkg] = &testPkg{
name: pkg,
tests: orderedmap.OrderedMap[string, testCase]{},
}
instance.tests[pkg].tests[testName] = testCase{
Package: pkg,
Name: testName,
tester: succeeds(&tt),
}
}

pkgs := ListPackages()
require.Equal(t, []PackageSpec{
{Name: "github.com/gametimesf/testy/packageapi/pkg0", Index: 0},
{Name: "github.com/gametimesf/testy/packageapi/pkg1", Index: 1},
}, pkgs)

start := time.Now()
packageResults := make([]TestResult, len(pkgs))
for _, pkg := range pkgs {
res, err := RunPackage(pkg.Name)
require.NoError(t, err)
packageResults[pkg.Index] = res
}
suite := BuildSuiteResult(start, packageResults)

assert.Equal(t, ResultPassed, suite.Result)
require.Len(t, suite.Subtests, 2)
assert.Equal(t, "github.com/gametimesf/testy/packageapi/pkg0", suite.Subtests[0].Package)
assert.Equal(t, "github.com/gametimesf/testy/packageapi/pkg1", suite.Subtests[1].Package)
}

func TestRunPackageReturnsErrPackageNotFound(t *testing.T) {
instance = testy{}
defer func() {
instance = testy{}
}()

_, err := RunPackage("github.com/gametimesf/testy/missing")
require.Error(t, err)
assert.True(t, errors.Is(err, ErrPackageNotFound))
}
func panics(ts *time.Time) Tester {
return func(TestingT) {
*ts = time.Now()
Expand Down
Loading