Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6a00b7a
draft
metafates Jun 15, 2026
4a60330
remove external dependencies
metafates Jun 16, 2026
819be66
simplify loader
metafates Jun 17, 2026
5c47fc7
remove go.sum
metafates Jun 17, 2026
dda8da6
Merge branch 'main' into testo-cmd
metafates Jun 18, 2026
39b9164
add private cases fields linter
metafates Jun 18, 2026
f566211
add json flag
metafates Jun 18, 2026
b026227
add logo
metafates Jun 18, 2026
a00e7eb
add version command
metafates Jun 18, 2026
3fe1fe3
better cli ux
metafates Jun 18, 2026
8fdb5ec
improve lint output
metafates Jun 18, 2026
3d85ec5
move cmd packages to internal
metafates Jun 18, 2026
38c3733
add bench
metafates Jun 19, 2026
e44bc6f
skip object resolution
metafates Jun 19, 2026
8d628a5
skip parsing for export data
metafates Jun 19, 2026
e72ed20
rename -testo flag to -pkg
metafates Jun 20, 2026
79b232b
run draft
metafates Jun 20, 2026
4913154
run draft
metafates Jun 20, 2026
8047619
allow running without patterns
metafates Jun 21, 2026
95ec21c
improve selectors
metafates Jun 21, 2026
12b80c9
allow only one pattern
metafates Jun 21, 2026
cfd9349
improve cli
metafates Jun 21, 2026
6af8c45
add exit errors
metafates Jun 21, 2026
fc0a36f
minor improvements
metafates Jun 22, 2026
14f62da
call gopls for references
metafates Jul 1, 2026
03c7245
minor improvements
metafates Jul 4, 2026
210329f
improvements
metafates Jul 4, 2026
40405e9
do not rely on gopls
metafates Jul 4, 2026
200eb92
cache runners
metafates Jul 4, 2026
fd7bba3
better suites command flags
metafates Jul 4, 2026
1aa7b4e
improve format options
metafates Jul 4, 2026
de21ac8
improve format output
metafates Jul 5, 2026
bc3429a
refactor cmd packages
metafates Jul 5, 2026
90fef28
fix newlines in lint cmd
metafates Jul 5, 2026
9dd81d6
show types in suites cmd from source code
metafates Jul 5, 2026
063652f
add tags command
metafates Jul 5, 2026
5d6ebc0
improve tags command
metafates Jul 6, 2026
84a2662
derive build tags if not passed
metafates Jul 6, 2026
8ffebdd
show lint usage
metafates Jul 6, 2026
a709861
pass build tags for run
metafates Jul 6, 2026
af11c6b
overall cmd improvements
metafates Jul 12, 2026
7f7e9e6
support -h without subcommand, update readme
metafates Jul 12, 2026
b287546
exclude cmd from coverage
metafates Jul 12, 2026
a5e615a
update README
metafates Jul 12, 2026
6dfb15a
update README
metafates Jul 12, 2026
bcc0688
add comparison with other frameworks
metafates Jul 12, 2026
debabc7
update README
metafates Jul 12, 2026
0eb9902
Merge branch 'main' into testo-cmd
metafates Jul 13, 2026
59a2339
improve suites template
metafates Jul 14, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Run unit tests without race detector
if: runner.os == 'Windows'
run: |
go test -v -cover -coverpkg=./... ./...
go test -v -cover -coverpkg=.,./testo...,./internal/... ./...

- name: Test examples output
run: go test -v -tags e2e -count=1 ./examples_test.go
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

## Added

- Auxiliary command line tool for Testo featuring linter, suites explorer and runner.

## [1.5.1] - 2026-06-17

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ doc:

# get test coverage
coverage:
go test -coverprofile=coverage.out -coverpkg=./... ./...
go test -coverprofile=coverage.out -coverpkg=.,./testo...,./internal/... ./...
go tool cover -func coverage.out

# visualize test coverage
coverage-html: coverage
go tool cover -html coverage.out

install:
go install ./cmd/testo
go install ./cmd/...

update-examples-output:
./update-examples-output.sh
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Add some flavor to your tests with
- Test reflection - deeply inspect test's meta-information.
- Caching - key-value storage persistent between test runs.
- [Zero dependencies](./go.mod).
- [Auxiliary command line tool](./cmd/testo) - linter, suites explorer and runner.

## Why Testo

Expand Down Expand Up @@ -112,6 +113,25 @@ Makes it easier to run and debug individual suite tests and adds helpful snippet

![VSCode extension screenshot showing codelens buttons for running and debugging a test](./vscode-extension/example.png)

## Testo command line tool

Testo has an auxiliary command line tool featuring linter, suites explorer and runner.

[See more here](./cmd/testo).

Example:

```bash
go install github.com/ozontech/testo/cmd/testo

testo lint ./...
testo run mypkg/Functional.TestFoo
testo suites -f "{{ .Package }}/{{ .Suite }}" | fzf
```

> [!NOTE]
> This command line tool is _completely optional_ and _is not required_ to run tests.

## Minimum supported Go version

Testo guarantees to support at least **3 latest major** [Go releases](https://go.dev/doc/devel/release).
Expand Down
31 changes: 31 additions & 0 deletions cmd/testo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Testo CTL

Auxiliary command line tool for Testo featuring linter, suites explorer and runner.

> [!WARNING]
> This tool is experimental, handle with care;
> may change without warning

## Install

```bash
go install github.com/ozontech/testo/cmd/testo
```

## Usage

Run `testo -h` to see available commands:

```txt
Usage:
testo [command]

Available Commands:
lint Run testo linter
run Run testo suites
suites Show testo suites
tags Show project build tags
version Show testo version
```

Run `testo [command] -h` to show help for the given command.
212 changes: 212 additions & 0 deletions cmd/testo/internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package cli

import (
"bytes"
"cmp"
_ "embed"
"errors"
"flag"
"fmt"
"maps"
"os"
"slices"
)

//go:embed logo.txt
var logo string

func usage(f *flag.FlagSet) {
var buf bytes.Buffer

fmt.Fprint(&buf, logo)
fmt.Fprintln(&buf, "Usage:")
fmt.Fprintf(&buf, " %s [command]\n\n", os.Args[0])
fmt.Fprintln(&buf, "Available Commands:")

for _, cmd := range slices.Sorted(maps.Keys(commands)) {
fmt.Fprintf(&buf, " %-10s %s\n", cmd, commands[cmd].Short)
}

f.Output().Write(buf.Bytes())
f.PrintDefaults()
}

func Run() {
flag.Usage = func() {
usage(flag.CommandLine)
}

if len(os.Args) < 2 {
flag.Parse()
flag.Usage()

os.Exit(2)
}

if err := run(os.Args[1], os.Args[2:]...); err != nil {
var errExit ExitError

if !errors.As(err, &errExit) {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}

errExit.Print()
os.Exit(errExit.Code)
}
}

func run(command string, args ...string) error {
switch command {
case "-h", "-help", "--help":
usage(flag.CommandLine)

return nil
}

r, ok := commands[command]
if !ok {
fmt.Fprintf(flag.CommandLine.Output(), "unknown subcommand: %q\n\n", command)

usage(flag.CommandLine)
os.Exit(2)

return nil
}

return r.Run(args...)
}

type Command interface {
Run(args ...string) error
}

var commands = make(map[string]registered)

type registered struct {
Short string
Run func(args ...string) error
}

type config struct {
Short string
Usage string
Long string
Args ArgsFunc
}

type Option func(conf *config)

func WithShort(short string) Option {
return func(conf *config) {
conf.Short = short
}
}

func WithUsage(u string) Option {
return func(conf *config) {
conf.Usage = u
}
}

func WithLong(u string) Option {
return func(conf *config) {
conf.Long = u
}
}

func WithoutArgs() Option {
return func(conf *config) {
conf.Args = func(args ...string) error {
if len(args) == 0 {
return nil
}

return fmt.Errorf("unexpected argument: %q", args[0])
}
}
}

type ArgsFunc func(args ...string) error

func Add[C Command](name string, flags func(f *flag.FlagSet, cmd *C), options ...Option) {
var conf config

for _, o := range options {
o(&conf)
}

var command C

commands[name] = registered{
Short: conf.Short,
Run: func(args ...string) error {
f := flag.NewFlagSet(name, flag.ExitOnError)

flags(f, &command)

f.Usage = func() {
long := cmp.Or(conf.Long, conf.Short)

if long != "" {
fmt.Fprintf(f.Output(), "%s\n\n", long)
}

fmt.Fprintln(f.Output(), "Usage:")

var hasFlags bool

f.VisitAll(func(*flag.Flag) { hasFlags = true })

if conf.Usage != "" {
fmt.Fprintf(f.Output(), " %s %s %s\n", os.Args[0], name, conf.Usage)
} else if hasFlags {
fmt.Fprintf(f.Output(), " %s %s [flags]\n", os.Args[0], name)
} else {
fmt.Fprintf(f.Output(), " %s %s\n", os.Args[0], name)
}

if hasFlags {
fmt.Fprintln(f.Output(), "\nFlags:")

f.PrintDefaults()
}
}

if err := parseFlagSet(f, args); err != nil {
return err
}

positional := f.Args()

if conf.Args != nil {
if err := conf.Args(positional...); err != nil {
return fmt.Errorf("%s %s: %w", os.Args[0], name, err)
}
}

return command.Run(positional...)
},
}
}

func parseFlagSet(f *flag.FlagSet, args []string) error {
positional := make([]string, 0, len(args))

for {
if err := f.Parse(args); err != nil {
return err
}

args = args[len(args)-f.NArg():]
if len(args) == 0 {
break
}

positional = append(positional, args[0])

args = args[1:]
}

return f.Parse(positional)
}
45 changes: 45 additions & 0 deletions cmd/testo/internal/cli/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cli

import (
"fmt"
"os"
)

type ExitError struct {
Code int

stdout string
stderr string
}

func (e ExitError) Error() string {
return fmt.Sprintf("exit code %d", e.Code)
}

func Exit(code int) ExitError {
return ExitError{
Code: code,
}
}

func (e ExitError) Stdout(s string) ExitError {
e.stdout = s

return e
}

func (e ExitError) Stderr(s string) ExitError {
e.stderr = s

return e
}

func (e ExitError) Print() {
if e.stdout != "" {
fmt.Fprint(os.Stdout, e.stdout)
}

if e.stderr != "" {
fmt.Fprint(os.Stderr, e.stderr)
}
}
Loading