Repository: gotestyourself/gotestsum
Pull request #359 (2023-08-13 14:34:08)

Add testdox format
This format shows test names as full sentences, which is useful both for autogenerating documentation ("dox"), and for encouraging programmers to write their test names as sentences describing the behaviour of the unit under test.

The specific method of formatting test names as sentences is that used by https://github.com/bitfield/gotestdox.

bitfield: > I pushed a commit to use the same icons we use in other formats. There's a --format-hivis option to change the icons to more colorful ones. In a future version we might look at making those the default.

I leave that up to you, of course. But you might be interested to know why `gotestdox` uses the emojis it does. One of my first users was a blind developer who uses a screen reader, and he reported that the ✘ emoji I was using is pronounced by the screen reader as something like "heavy ballot x", which is weird (`gotestsum`'s is pronounced "heavy multiplication x", which is even weirder). Changing it to simply `x` made the output a lot more accessible for him.
dnephin: Ah, I had not considered screen readers but that's a great point! I'd love to provide some better support. Maybe `--format-audible` or something like that could change the `x` to "failed test", etc?  The pattern here for switching between the original icons and the new hivis icons seems like it would make it easy to switch to other options as well.

Thank you for explaining the design decisions in gotestdox!

Patch:
diff --git a/README.md b/README.md
index ed86817..616eb54 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ Commonly used formats (see `--help` for a full list):
  * `dots` - print a character for each test.
  * `pkgname` (default) - print a line for each package.
  * `testname` - print a line for each test and package.
+ * `testdox` - print a sentence for each test using [gotestdox](https://github.com/bitfield/gotestdox).
  * `standard-quiet` - the standard `go test` format.
  * `standard-verbose` - the standard `go test -v` format.
 
diff --git a/cmd/main.go b/cmd/main.go
index 86df767..cb522ab 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -140,6 +140,7 @@ Formats:
     pkgname                  print a line for each package
     pkgname-and-test-fails   print a line for each package and failed test output
     testname                 print a line for each test and package
+    testdox                  print a sentence for each test using gotestdox
     github-actions           testname format with github actions log grouping
     standard-quiet           standard go test format
     standard-verbose         standard go test -v format
diff --git a/go.mod b/go.mod
index 86e78cd..5e5ef66 100644
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,16 @@
 module gotest.tools/gotestsum
 
 require (
+	github.com/bitfield/gotestdox v0.2.1
 	github.com/dnephin/pflag v1.0.7
-	github.com/fatih/color v1.13.0
+	github.com/fatih/color v1.15.0
 	github.com/fsnotify/fsnotify v1.5.4
-	github.com/google/go-cmp v0.5.8
+	github.com/google/go-cmp v0.5.9
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
-	github.com/mattn/go-colorable v0.1.12 // indirect
-	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
-	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
-	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
-	golang.org/x/tools v0.1.11
+	golang.org/x/sync v0.3.0
+	golang.org/x/sys v0.10.0
+	golang.org/x/term v0.10.0
+	golang.org/x/tools v0.11.0
 	gotest.tools/v3 v3.3.0
 )
 
diff --git a/go.sum b/go.sum
index 8f8ed3b..cbd23a8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,67 +1,87 @@
+github.com/bitfield/gotestdox v0.2.1 h1:Zj8IMLAO5/oiAKoMmtN96eyFiPZraJRTH2p0zDgtxc0=
+github.com/bitfield/gotestdox v0.2.1/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
 github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
 github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
-golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
-golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
+golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=


Test patch:
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index 4ff8982..5a2e2aa 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -36,6 +36,7 @@ Formats:
     pkgname                  print a line for each package
     pkgname-and-test-fails   print a line for each package and failed test output
     testname                 print a line for each test and package
+    testdox                  print a sentence for each test using gotestdox
     github-actions           testname format with github actions log grouping
     standard-quiet           standard go test format
     standard-verbose         standard go test -v format
diff --git a/testjson/format.go b/testjson/format.go
index f298eb0..8efc295 100644
--- a/testjson/format.go
+++ b/testjson/format.go
@@ -5,8 +5,10 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"sort"
 	"strings"
 
+	"github.com/bitfield/gotestdox"
 	"github.com/fatih/color"
 )
 
@@ -77,11 +79,65 @@ func standardJSONFormat(out io.Writer) EventFormatter {
 func testNameFormatTestEvent(out io.Writer, event TestEvent) {
 	pkgPath := RelativePackagePath(event.Package)
 
-	fmt.Fprintf(out, "%s %s%s %s\n",
+	fmt.Fprintf(out, "%s %s%s (%.2fs)\n",
 		colorEvent(event)(strings.ToUpper(string(event.Action))),
 		joinPkgToTestName(pkgPath, event.Test),
 		formatRunID(event.RunID),
-		fmt.Sprintf("(%.2fs)", event.Elapsed))
+		event.Elapsed)
+}
+
+func testDoxFormat(out io.Writer, opts FormatOptions) EventFormatter {
+	buf := bufio.NewWriter(out)
+	type Result struct {
+		Event    TestEvent
+		Sentence string
+	}
+	getIcon := icon
+	if opts.UseHiVisibilityIcons {
+		getIcon = iconHiVis
+	}
+	results := map[string][]Result{}
+	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
+		switch {
+		case event.PackageEvent():
+			if !event.Action.IsTerminal() {
+				return nil
+			}
+			if opts.HideEmptyPackages && len(results[event.Package]) == 0 {
+				return nil
+			}
+			fmt.Fprintf(buf, "%s:\n", event.Package)
+			tests := results[event.Package]
+			sort.Slice(tests, func(i, j int) bool {
+				return tests[i].Sentence < tests[j].Sentence
+			})
+			for _, r := range tests {
+				fmt.Fprintf(buf, " %s %s (%.2fs)\n",
+					getIcon(r.Event.Action),
+					r.Sentence,
+					r.Event.Elapsed)
+			}
+			fmt.Fprintln(buf)
+			return buf.Flush()
+		case event.Action.IsTerminal():
+			// Fuzz test cases tend not to have interesting names,
+			// so only report these if they're failures
+			if isFuzzCase(event) {
+				return nil
+			}
+			results[event.Package] = append(results[event.Package], Result{
+				Event:    event,
+				Sentence: gotestdox.Prettify(event.Test),
+			})
+		}
+		return nil
+	})
+}
+
+func isFuzzCase(event TestEvent) bool {
+	return strings.HasPrefix(event.Test, "Fuzz") &&
+		event.Action == ActionPass &&
+		TestName(event.Test).IsSubTest()
 }
 
 func testNameFormat(out io.Writer) EventFormatter {
@@ -188,40 +244,59 @@ func pkgNameFormat(out io.Writer, opts FormatOptions) eventFormatterFunc {
 	}
 }
 
+func icon(action Action) string {
+	switch action {
+	case ActionPass:
+		return color.GreenString("✓")
+	case ActionSkip:
+		return color.YellowString("∅")
+	case ActionFail:
+		return color.RedString("✖")
+	default:
+		return ""
+	}
+}
+
+func iconHiVis(action Action) string {
+	switch action {
+	case ActionPass:
+		return "✅"
+	case ActionSkip:
+		return "➖"
+	case ActionFail:
+		return "❌"
+	default:
+		return ""
+	}
+}
+
 func shortFormatPackageEvent(opts FormatOptions, event TestEvent, exec *Execution) string {
 	pkg := exec.Package(event.Package)
 
-	var iconSkipped, iconSuccess, iconFailure string
+	getIcon := icon
 	if opts.UseHiVisibilityIcons {
-		iconSkipped = "➖"
-		iconSuccess = "✅"
-		iconFailure = "❌"
-	} else {
-		iconSkipped = "∅"
-		iconSuccess = "✓"
-		iconFailure = "✖"
+		getIcon = iconHiVis
 	}
 
 	fmtEvent := func(action string) string {
 		return action + "  " + packageLine(event, exec.Package(event.Package))
 	}
-	withColor := colorEvent(event)
 	switch event.Action {
 	case ActionSkip:
 		if opts.HideEmptyPackages {
 			return ""
 		}
-		return fmtEvent(withColor(iconSkipped))
+		return fmtEvent(getIcon(event.Action))
 	case ActionPass:
 		if pkg.Total == 0 {
 			if opts.HideEmptyPackages {
 				return ""
 			}
-			return fmtEvent(withColor(iconSkipped))
+			return fmtEvent(getIcon(ActionSkip))
 		}
-		return fmtEvent(withColor(iconSuccess))
+		return fmtEvent(getIcon(event.Action))
 	case ActionFail:
-		return fmtEvent(withColor(iconFailure))
+		return fmtEvent(getIcon(event.Action))
 	}
 	return ""
 }
@@ -312,6 +387,8 @@ func NewEventFormatter(out io.Writer, format string, formatOpts FormatOptions) E
 		return dotsFormatV1(out)
 	case "dots-v2":
 		return newDotFormatter(out, formatOpts)
+	case "gotestdox", "testdox":
+		return testDoxFormat(out, formatOpts)
 	case "testname", "short-verbose":
 		if os.Getenv("GITHUB_ACTIONS") == "true" {
 			return githubActionsFormat(out)
diff --git a/testjson/format_test.go b/testjson/format_test.go
index b2b4605..fed4862 100644
--- a/testjson/format_test.go
+++ b/testjson/format_test.go
@@ -86,6 +86,13 @@ func TestFormats_DefaultGoTestJson(t *testing.T) {
 	}
 
 	testCases := []testCase{
+		{
+			name: "testdox",
+			format: func(out io.Writer) EventFormatter {
+				return testDoxFormat(out, FormatOptions{})
+			},
+			expectedOut: "format/testdox.out",
+		},
 		{
 			name:        "testname",
 			format:      testNameFormat,
@@ -176,6 +183,13 @@ func TestFormats_Coverage(t *testing.T) {
 	}
 
 	testCases := []testCase{
+		{
+			name: "testdox",
+			format: func(out io.Writer) EventFormatter {
+				return testDoxFormat(out, FormatOptions{})
+			},
+			expectedOut: "format/testdox-coverage.out",
+		},
 		{
 			name:        "testname",
 			format:      testNameFormat,
@@ -244,6 +258,13 @@ func TestFormats_Shuffle(t *testing.T) {
 	}
 
 	testCases := []testCase{
+		{
+			name: "testdox",
+			format: func(out io.Writer) EventFormatter {
+				return testDoxFormat(out, FormatOptions{})
+			},
+			expectedOut: "format/testdox-shuffle.out",
+		},
 		{
 			name:        "testname",
 			format:      testNameFormat,
diff --git a/testjson/testdata/format/testdox-coverage.out b/testjson/testdata/format/testdox-coverage.out
new file mode 100644
index 0000000..fe2d7c3
--- /dev/null
+++ b/testjson/testdata/format/testdox-coverage.out
@@ -0,0 +1,52 @@
+gotest.tools/gotestsum/testjson/internal/badmain:
+
+gotest.tools/gotestsum/testjson/internal/good:
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ✓ With stderr (0.00s)
+
+gotest.tools/gotestsum/testjson/internal/stub:
+ ✖ Failed (0.00s)
+ ✖ Failed with stderr (0.00s)
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✖ Nested with failure (0.00s)
+ ✓ Nested with failure a (0.00s)
+ ✓ Nested with failure a sub (0.00s)
+ ✓ Nested with failure b (0.00s)
+ ✓ Nested with failure b sub (0.00s)
+ ✖ Nested with failure c (0.00s)
+ ✓ Nested with failure d (0.00s)
+ ✓ Nested with failure d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ✓ With stderr (0.00s)
+
diff --git a/testjson/testdata/format/testdox-shuffle.out b/testjson/testdata/format/testdox-shuffle.out
new file mode 100644
index 0000000..825669b
--- /dev/null
+++ b/testjson/testdata/format/testdox-shuffle.out
@@ -0,0 +1,67 @@
+gotest.tools/gotestsum/testjson/internal/badmain:
+
+gotest.tools/gotestsum/testjson/internal/good:
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ✓ With stderr (0.00s)
+
+gotest.tools/gotestsum/testjson/internal/parallelfails:
+ ✖ Nested parallel failures (0.00s)
+ ✖ Nested parallel failures a (0.00s)
+ ✖ Nested parallel failures b (0.00s)
+ ✖ Nested parallel failures c (0.00s)
+ ✖ Nested parallel failures d (0.00s)
+ ✖ Parallel the first (0.01s)
+ ✖ Parallel the second (0.01s)
+ ✖ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ✓ With stderr (0.00s)
+
+gotest.tools/gotestsum/testjson/internal/withfails:
+ ✖ Failed (0.00s)
+ ✖ Failed with stderr (0.00s)
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✖ Nested with failure (0.00s)
+ ✓ Nested with failure a (0.00s)
+ ✓ Nested with failure a sub (0.00s)
+ ✓ Nested with failure b (0.00s)
+ ✓ Nested with failure b sub (0.00s)
+ ✖ Nested with failure c (0.00s)
+ ✓ Nested with failure d (0.00s)
+ ✓ Nested with failure d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ∅ Timeout (0.00s)
+ ✓ With stderr (0.00s)
+
diff --git a/testjson/testdata/format/testdox.out b/testjson/testdata/format/testdox.out
new file mode 100644
index 0000000..028e826
--- /dev/null
+++ b/testjson/testdata/format/testdox.out
@@ -0,0 +1,69 @@
+gotest.tools/gotestsum/testjson/internal/badmain:
+
+gotest.tools/gotestsum/testjson/internal/empty:
+
+gotest.tools/gotestsum/testjson/internal/good:
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ✓ With stderr (0.00s)
+
+gotest.tools/gotestsum/testjson/internal/parallelfails:
+ ✖ Nested parallel failures (0.00s)
+ ✖ Nested parallel failures a (0.00s)
+ ✖ Nested parallel failures b (0.00s)
+ ✖ Nested parallel failures c (0.00s)
+ ✖ Nested parallel failures d (0.00s)
+ ✖ Parallel the first (0.01s)
+ ✖ Parallel the second (0.01s)
+ ✖ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ✓ With stderr (0.00s)
+
+gotest.tools/gotestsum/testjson/internal/withfails:
+ ✖ Failed (0.00s)
+ ✖ Failed with stderr (0.00s)
+ ✓ Nested success (0.00s)
+ ✓ Nested success a (0.00s)
+ ✓ Nested success a sub (0.00s)
+ ✓ Nested success b (0.00s)
+ ✓ Nested success b sub (0.00s)
+ ✓ Nested success c (0.00s)
+ ✓ Nested success c sub (0.00s)
+ ✓ Nested success d (0.00s)
+ ✓ Nested success d sub (0.00s)
+ ✖ Nested with failure (0.00s)
+ ✓ Nested with failure a (0.00s)
+ ✓ Nested with failure a sub (0.00s)
+ ✓ Nested with failure b (0.00s)
+ ✓ Nested with failure b sub (0.00s)
+ ✖ Nested with failure c (0.00s)
+ ✓ Nested with failure d (0.00s)
+ ✓ Nested with failure d sub (0.00s)
+ ✓ Parallel the first (0.01s)
+ ✓ Parallel the second (0.01s)
+ ✓ Parallel the third (0.00s)
+ ✓ Passed (0.00s)
+ ✓ Passed with log (0.00s)
+ ✓ Passed with stdout (0.00s)
+ ∅ Skipped (0.00s)
+ ∅ Skipped wit log (0.00s)
+ ∅ Timeout (0.00s)
+ ✓ With stderr (0.00s)
+