diff --git a/tools/spec_coverage/README.md b/tools/spec_coverage/README.md new file mode 100644 index 0000000000..c7d60be201 --- /dev/null +++ b/tools/spec_coverage/README.md @@ -0,0 +1,16 @@ +Command-line application that shows Dart specification coverage by co19 tests. + +// FIXME (sgrekhov) +For now this tool takes a .txt version of the Dart specification as an input. +It obtained from latex version as follows: + +pip install pylatexenc +latex2text dartLangSpec.tex > spec.txt + +Path to `spec.txt` is specified in `config/config.json` + +// End of FIXME + +Usage: +`$ cd Tools/spec_coverage` +`$ dart bin/main.dart` diff --git a/tools/spec_coverage/bin/main.dart b/tools/spec_coverage/bin/main.dart new file mode 100644 index 0000000000..fec2a6c1b5 --- /dev/null +++ b/tools/spec_coverage/bin/main.dart @@ -0,0 +1,44 @@ +import 'dart:convert' show jsonDecode; +import 'dart:io'; +import 'package:spec_coverage/co19.dart'; +import 'package:spec_coverage/configuration.dart'; +import 'package:spec_coverage/specification.dart'; + +main() { + Configuration config = Configuration.fromJson(readConfig()); + Specification spec = Specification.fromTxt(config.specPath); + Co19 co19 = Co19(config.co19Dir); + compareChaptersAndDirs(co19.language.subDirs, spec.chapters); +} + +void compareChaptersAndDirs(List testDirs, List chapters) { + for (TestDir td in testDirs) { + bool found = false; + for(Chapter ch in chapters) { + if (ch.comparableName.compareTo(td.comparableName) > 0) { + break; + } else if (td.comparableName == ch.comparableName) { + print("Found spec for ${td.path}"); + compareChaptersAndDirs(td.subDirs, ch.subChapters); + found = true; + break; + } + } + if (!found) { + print("Not found spec for ${td.path}. Chapters are:"); + for (Chapter ch in chapters) { + print(ch.comparableName); + } + } + } +} + +Map readConfig() { + final configFile = File('configuration/configuration.json'); + if (configFile.existsSync()) { + final contents = configFile.readAsStringSync(); + return jsonDecode(contents); + } else { + throw Exception('Config file ${configFile.path} not found'); + } +} diff --git a/tools/spec_coverage/configuration/configuration.json b/tools/spec_coverage/configuration/configuration.json new file mode 100644 index 0000000000..68248e31ba --- /dev/null +++ b/tools/spec_coverage/configuration/configuration.json @@ -0,0 +1,4 @@ +{ + "specPath": "resources/spec.txt", + "co19Dir": "../.." +} diff --git a/tools/spec_coverage/lib/co19.dart b/tools/spec_coverage/lib/co19.dart new file mode 100644 index 0000000000..b8586254bb --- /dev/null +++ b/tools/spec_coverage/lib/co19.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +class Co19 { + static const languageDirName = "Language"; + late final TestDir root; + late final TestDir language; + + Co19(String path) { + Directory dir = Directory(path); + if (!dir.existsSync()) { + throw Exception("Directory '$path' does not exist"); + } + root = TestDir(dir); + for (TestDir td in root.subDirs) { + if (td.name == languageDirName) { + language = td; + break; + } + } + } +} + +class TestDir { + static final _filenamePattern = RegExp(r"_t[0-9]{2,3}.dart"); + final String path; + late final String name; + late final String comparableName; + final List subDirs = []; + final List tests = []; + + TestDir(Directory root) : path = resolvePath(root.path) { + name = _entityName(path); + comparableName = name.toLowerCase().replaceAll("-", "_"); + List entities = root.listSync(); + for (FileSystemEntity fse in entities) { + if (_skip(fse)) { + continue; + } + FileStat fs = fse.statSync(); + if (fs.type == FileSystemEntityType.directory) { + subDirs.add(TestDir(Directory(fse.path))); + } else if (fs.type == FileSystemEntityType.file) { + if (!_filenamePattern.hasMatch(_entityName(fse.path))) { + continue; + } + tests.add(TestFile(fse.path)); + } + } + subDirs.sort( + (TestDir d1, TestDir d2) => + d1.comparableName.compareTo(d2.comparableName)); + } + + static String resolvePath(String relativePath) { + String basePath = Directory.current.path; + Uri baseUri = Uri.directory(basePath); + Uri resolvedUri = baseUri.resolve(relativePath); + return resolvedUri.toFilePath(); + } + + String _entityName(String p) => + p.substring(p.lastIndexOf(Platform.pathSeparator) + 1); + + bool _skip(FileSystemEntity fse) { + // Skip directories like .git + if (_entityName(fse.path).startsWith(RegExp(r"\.[a-zA-Z]+"))) { + return true; + } + return false; + } + + @override + String toString() => comparableName; +} + +class TestFile { + final String path; + + TestFile(this.path); +} diff --git a/tools/spec_coverage/lib/configuration.dart b/tools/spec_coverage/lib/configuration.dart new file mode 100644 index 0000000000..e1c5b931b9 --- /dev/null +++ b/tools/spec_coverage/lib/configuration.dart @@ -0,0 +1,8 @@ +class Configuration { + final String co19Dir; + final String specPath; + + Configuration.fromJson(Map json) + : co19Dir = json["co19Dir"], + specPath = json["specPath"]; +} diff --git a/tools/spec_coverage/lib/spec_parser.dart b/tools/spec_coverage/lib/spec_parser.dart new file mode 100644 index 0000000000..d548dd4baa --- /dev/null +++ b/tools/spec_coverage/lib/spec_parser.dart @@ -0,0 +1,61 @@ +import 'specification.dart'; + +class SpecParser { + static const pchar = "ยง"; + int chapterCounter1 = 0; + int chapterCounter2 = 0; + int chapterCounter3 = 0; + + List parse(List lines) { + return _parseLevel(null, lines, 1); + } + + List _parseLevel(Chapter? ch, List lines, int level) { + final List parsed = []; + final List chLines = []; + + Pattern pattern = switch (level) { + 1 => RegExp("$pchar [a-zA-Z]{2,}"), + 2 => RegExp(" $pchar.$pchar [a-zA-Z]{2,}"), + 3 => RegExp(" $pchar.$pchar.$pchar [a-zA-Z]{2,}"), + _ => throw "Wrong level number $level", + }; + + while (lines.isNotEmpty) { + String line = lines.removeAt(0); + if (line.startsWith(pattern)) { + if (ch != null) { + if (level < 3) { + ch.subChapters = (_parseLevel(ch, chLines, level + 1)); + } + ch.lines.addAll(chLines); + chLines.clear(); + } + ChapterNumber cn = _getChapterNumber(line); + int start = cn.numbers.length == 1 ? 2 : 2 * cn.numbers.length + 1; + ch = Chapter(number: cn, header: line.substring(start).trim()); + parsed.add(ch); + } else if (ch != null) { + chLines.add(line); + } + } + return parsed; + } + + ChapterNumber _getChapterNumber(String line) { + if (line.startsWith(" $pchar.$pchar.$pchar")) { + return ChapterNumber([ + chapterCounter1, + chapterCounter2, + ++chapterCounter3, + ]); + } + if (line.startsWith(" $pchar.$pchar")) { + chapterCounter3 = 0; + return ChapterNumber([chapterCounter1, ++chapterCounter2]); + } + chapterCounter2 = 0; + chapterCounter3 = 0; + return ChapterNumber([++chapterCounter1]); + } +} diff --git a/tools/spec_coverage/lib/specification.dart b/tools/spec_coverage/lib/specification.dart new file mode 100644 index 0000000000..a8dd895e24 --- /dev/null +++ b/tools/spec_coverage/lib/specification.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:spec_coverage/spec_parser.dart'; + +class Specification { + final List chapters = []; + + Specification.fromTxt(String path) { + File file = File(path); + List lines = file.readAsLinesSync(); + SpecParser sp = SpecParser(); + chapters.addAll(sp.parse(lines)); + chapters.sort( + (Chapter ch1, Chapter ch2) => + ch1.comparableName.compareTo(ch2.comparableName), + ); + } +} + +class Chapter { + final ChapterNumber number; + final String header; + final String comparableName; + final List _subChapters = []; + final List lines = []; + + Chapter({required this.number, required this.header}) + : comparableName = + header.replaceAll(" ", "_").replaceAll("-", "_").toLowerCase(); + + List get subChapters => _subChapters; + + void set subChapters(List val) { + _subChapters.addAll(val); + _subChapters.sort( + (Chapter ch1, Chapter ch2) => + ch1.comparableName.compareTo(ch2.comparableName)); + } + + @override + String toString() => "$number $header"; +} + +class ChapterNumber { + final List numbers; + + ChapterNumber(this.numbers); + + @override + String toString() => numbers.join("."); +} diff --git a/tools/spec_coverage/pubspec.yaml b/tools/spec_coverage/pubspec.yaml new file mode 100644 index 0000000000..0366bbeab7 --- /dev/null +++ b/tools/spec_coverage/pubspec.yaml @@ -0,0 +1,9 @@ +name: spec_coverage +description: "Dart specification coverage CLI tool" + +publish_to: 'none' + +version: 0.0.1 + +environment: + sdk: '^3.7.0'