#!/usr/bin/env dart import 'dart:convert'; import 'dart:io'; import 'package:logger/web.dart'; /// Converts Flutter JSON test results to JUnit XML format /// Usage: dart test_runner.dart [test_results.json] [output.xml] void main(List arguments) { var logger = Logger( printer: PrettyPrinter(methodCount: 2, errorMethodCount: 8, colors: true, printEmojis: true, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart), ); if (arguments.length < 2) { logger.i('Usage: dart test_runner.dart '); exit(1); } final jsonFile = arguments[0]; final xmlFile = arguments[1]; try { final jsonContent = File(jsonFile).readAsStringSync(); final lines = jsonContent.trim().split('\n'); final tests = []; int totalTests = 0; int failures = 0; int skipped = 0; double duration = 0.0; // Parse JSON lines from flutter test output for (final line in lines) { if (line.trim().isEmpty) continue; try { final data = jsonDecode(line) as Map; final type = data['type'] as String?; if (type == 'testStart') { final test = data['test'] as Map; final name = test['name'] as String; final id = test['id'] as int; tests.add(TestResult(id: id, name: name)); totalTests++; } else if (type == 'testDone') { final testId = data['testID'] as int; final result = data['result'] as String; final time = data['time'] as int? ?? 0; final error = data['error'] as String?; final stackTrace = data['stackTrace'] as String?; duration += time / 1000.0; // Convert to seconds final testIndex = tests.indexWhere((t) => t.id == testId); if (testIndex != -1) { tests[testIndex].result = result; tests[testIndex].duration = time / 1000.0; tests[testIndex].error = error; tests[testIndex].stackTrace = stackTrace; if (result == 'error' || result == 'failure') { failures++; } else if (result == 'skip') { skipped++; } } } } catch (e) { // Skip malformed JSON lines logger.i('Warning: Could not parse line: $line'); } } // Generate JUnit XML final xml = generateJUnitXML(tests, totalTests, failures, skipped, duration); File(xmlFile).writeAsStringSync(xml); logger.i('Converted ${tests.length} test results to JUnit XML: $xmlFile'); // Exit with error code if there were failures if (failures > 0) { exit(1); } } catch (e) { logger.e('Error: $e'); exit(1); } } class TestResult { final int id; final String name; String result = 'pending'; double duration = 0.0; String? error; String? stackTrace; TestResult({required this.id, required this.name}); } String generateJUnitXML(List tests, int total, int failures, int skipped, double duration) { final buffer = StringBuffer(); buffer.writeln(''); buffer.writeln(''); for (final test in tests) { final escapedName = _escapeXml(test.name); buffer.writeln(' '); if (test.result == 'error' || test.result == 'failure') { final escapedError = _escapeXml(test.error ?? 'Test failed'); final escapedStackTrace = _escapeXml(test.stackTrace ?? ''); buffer.writeln(' '); buffer.writeln(' '); buffer.writeln(' '); } else if (test.result == 'skip') { buffer.writeln(' '); } buffer.writeln(' '); } buffer.writeln(''); return buffer.toString(); } String _escapeXml(String text) { return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }