@@ -1113,6 +1113,154 @@ def _emit_test():
11131113 return 0
11141114
11151115
1116+ def GoJSON2SubUnit (gojson , output_stream ):
1117+ """Filter a `go test -json` stream into a subunit v2 byte stream.
1118+
1119+ `go test -json` (or `go tool test2json`) emits one JSON object per line.
1120+ Each object carries an `Action` (`run`, `output`, `pass`, `fail`, `skip`,
1121+ `pause`, `cont`, `bench`, `start`), a `Package`, an optional `Test`, and
1122+ on terminal events an `Elapsed` and `Time`. This function maps each
1123+ test's lifecycle to a pair of subunit packets — `inprogress` at the
1124+ `run` event and the final status at the terminal event — so the
1125+ consumer can derive a duration. Captured `output` lines for the test
1126+ are folded into a single `text/plain; charset=UTF8` attachment on the
1127+ terminal packet.
1128+
1129+ Test IDs are formed as ``<package>.<TestName>``. Subtests keep Go's
1130+ native ``Parent/Sub`` form, so the resulting ID is
1131+ ``<package>.<Parent>/<Sub>``, which round-trips through
1132+ ``go test -run '^Parent$/^Sub$'``.
1133+
1134+ Package-level failures (no `Test` field on a `fail` event — typically
1135+ a build error) are reported as a synthetic ``<package> [build]`` test
1136+ so the failure shows up alongside the test results instead of being
1137+ silently swallowed.
1138+
1139+ :param gojson: An iterable of text lines (e.g. ``sys.stdin``) carrying
1140+ the `go test -json` stream.
1141+ :param output_stream: A binary stream to write subunit v2 bytes to.
1142+ :return: 0 if no test failed, 1 otherwise — matching the convention
1143+ used by `TAP2SubUnit`.
1144+ """
1145+ import json
1146+
1147+ output = StreamResultToBytes (output_stream )
1148+ UTF8_TEXT = "text/plain; charset=UTF8"
1149+ # Per-test buffered `output` chunks plus the start timestamp captured
1150+ # on the `run` event, keyed by full test_id.
1151+ buffers = {}
1152+ start_times = {}
1153+ # Per-package buffered output, used to attribute build / setup failures
1154+ # that don't carry a `Test` field.
1155+ pkg_buffers = {}
1156+ any_failed = False
1157+
1158+ def parse_time (value ):
1159+ if not value :
1160+ return None
1161+ try :
1162+ return iso8601 .parse_date (value )
1163+ except (TypeError , ValueError , iso8601 .ParseError ):
1164+ return None
1165+
1166+ def make_test_id (pkg , test ):
1167+ # Both package and test are required for an unambiguous ID; the
1168+ # caller checks for `Test` before reaching here.
1169+ return "{}.{}" .format (pkg , test )
1170+
1171+ for line in gojson :
1172+ line = line .strip ()
1173+ if not line :
1174+ continue
1175+ try :
1176+ event = json .loads (line )
1177+ except (TypeError , ValueError ):
1178+ # `go test -json` occasionally interleaves a non-JSON banner
1179+ # (e.g. on a panic during package init). Drop it rather than
1180+ # aborting the whole stream.
1181+ continue
1182+ if not isinstance (event , dict ):
1183+ continue
1184+
1185+ action = event .get ("Action" )
1186+ pkg = event .get ("Package" ) or ""
1187+ test = event .get ("Test" )
1188+ timestamp = parse_time (event .get ("Time" ))
1189+
1190+ if action == "output" :
1191+ chunk = event .get ("Output" , "" )
1192+ if test :
1193+ buffers .setdefault (make_test_id (pkg , test ), []).append (chunk )
1194+ elif pkg :
1195+ pkg_buffers .setdefault (pkg , []).append (chunk )
1196+ continue
1197+
1198+ if action == "run" and test :
1199+ test_id = make_test_id (pkg , test )
1200+ start_times [test_id ] = timestamp
1201+ output .status (
1202+ test_id = test_id ,
1203+ test_status = "inprogress" ,
1204+ timestamp = timestamp ,
1205+ )
1206+ continue
1207+
1208+ if action in ("pass" , "fail" , "skip" ) and test :
1209+ status = {"pass" : "success" , "fail" : "fail" , "skip" : "skip" }[action ]
1210+ test_id = make_test_id (pkg , test )
1211+ chunks = buffers .pop (test_id , [])
1212+ start_times .pop (test_id , None )
1213+ file_bytes = ("" .join (chunks )).encode ("utf-8" ) if chunks else None
1214+ output .status (
1215+ test_id = test_id ,
1216+ test_status = status ,
1217+ eof = True ,
1218+ file_name = "go test output" if file_bytes else None ,
1219+ file_bytes = file_bytes ,
1220+ mime_type = UTF8_TEXT if file_bytes else None ,
1221+ timestamp = timestamp ,
1222+ )
1223+ if action == "fail" :
1224+ any_failed = True
1225+ continue
1226+
1227+ if action == "fail" and not test and pkg :
1228+ # Package-level failure (build error, init panic, etc.). Emit
1229+ # a synthetic test so the failure is visible.
1230+ chunks = pkg_buffers .pop (pkg , [])
1231+ file_bytes = ("" .join (chunks )).encode ("utf-8" ) if chunks else None
1232+ output .status (
1233+ test_id = "{} [build]" .format (pkg ),
1234+ test_status = "fail" ,
1235+ eof = True ,
1236+ file_name = "go test output" if file_bytes else None ,
1237+ file_bytes = file_bytes ,
1238+ mime_type = UTF8_TEXT if file_bytes else None ,
1239+ timestamp = timestamp ,
1240+ )
1241+ any_failed = True
1242+ continue
1243+
1244+ # `pass`/`skip` without `Test` is a package-level summary; harmless
1245+ # to drop. `pause`/`cont`/`start`/`bench` aren't terminal — skip.
1246+
1247+ # Any tests still in-progress at EOF were aborted (the runner died
1248+ # mid-test). Surface them as failures so they're not silently lost.
1249+ for test_id , chunks in list (buffers .items ()):
1250+ file_bytes = ("" .join (chunks )).encode ("utf-8" ) if chunks else None
1251+ output .status (
1252+ test_id = test_id ,
1253+ test_status = "fail" ,
1254+ eof = True ,
1255+ file_name = "go test output" if file_bytes else None ,
1256+ file_bytes = file_bytes ,
1257+ mime_type = UTF8_TEXT if file_bytes else None ,
1258+ )
1259+ any_failed = True
1260+
1261+ return 1 if any_failed else 0
1262+
1263+
11161264def tag_stream (original , filtered , tags ):
11171265 """Alter tags on a stream.
11181266
0 commit comments