Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
16 changes: 16 additions & 0 deletions tools/spec_coverage/README.md
Original file line number Diff line number Diff line change
@@ -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`
42 changes: 42 additions & 0 deletions tools/spec_coverage/bin/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:convert' show jsonDecode;
import 'dart:io';
import 'package:spec_coverage/co19.dart';
import 'package:spec_coverage/config.dart';
import 'package:spec_coverage/spec.dart';

main(List<String> args) {
Config config = Config.fromJson(readConfig());
Spec spec = Spec.fromTxt(config.specPath);
Co19 co19 = Co19(config.co19Dir);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Co19 goes along with Json, it's almost a word in this context. ;-)

findSpecChapters(co19.language.subDirs, spec.chapters);
}

void findSpecChapters(List<TestDir> testDirs, List<Chapter> chapters) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this name is confusing. findSpecChapters may describe what's going on, but it makes no sense to find something and then not deliver the entities that were found, or doing something to them, or with them.

Perhaps the code is not doing anything (other than print) because it's unfinished? In that case we might want to wait a bit until we have some code that is sufficiently complete to be understood and used in some concrete cases.

It would still make sense to land various pieces of code that are reasonably well-understood (e.g., the handling of configurations in a *.json file will probably not need to change much, but findSpecChapters looks like it will change substantially before we can run those scenarios).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to compareChaptersAndDirs

for (TestDir td in testDirs) {
bool found = false;
for(Chapter ch in chapters) {
if (td.name.toLowerCase() == ch.co19DirName.toLowerCase()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is approximately O(n^2) where n is the number of chapters-or-dirs. That's probably not too bad (we have about 208 sections/subsections/subsubsections, and there are about 505 co19 directories), but it still seems reasonable to sort each of those lists and take the toLowerCase once and for all, and then proceed to search for matching names by holding an index into each of those two lists. That would be more like linear time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Fixed! Please review.

print("Found spec for ${td.path}");
findSpecChapters(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.co19DirName);
}
}
}
}

Map<String, dynamic> readConfig() {
final configFile = File('config/config.json');
if (configFile.existsSync()) {
final contents = configFile.readAsStringSync();
return jsonDecode(contents);
} else {
throw Exception('Config file ${configFile.path} not found');
}
}
4 changes: 4 additions & 0 deletions tools/spec_coverage/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"specPath": "resources/spec.txt",
"co19Dir": "../.."
}
68 changes: 68 additions & 0 deletions tools/spec_coverage/lib/co19.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 exists");
}
root = TestDir(dir);
for (TestDir td in root.subDirs) {
if (td.name == languageDirName) {
language = td;
break;
}
}
}
}

class TestDir {
final String path;
final List<TestDir> subDirs = [];
final List<TestFile> tests = [];

TestDir(Directory root) : path = resolvePath(root.path) {
List<FileSystemEntity> 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) {
tests.add(TestFile(fse.path));
}
}
}

static String resolvePath(String relativePath) {
String basePath = Directory.current.path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this utility must be executed from the root of the package? OK, I think that's a rather common restriction (but it might be nice to have an error message if the current directory at startup doesn't look like a package root).

Uri baseUri = Uri.directory(basePath);
Uri resolvedUri = baseUri.resolve(relativePath);
return resolvedUri.toFilePath();
}

String get name => _entityName(path);

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;
}
}

class TestFile {
final String path;

TestFile(this.path);
}
8 changes: 8 additions & 0 deletions tools/spec_coverage/lib/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Config {
final String co19Dir;
final String specPath;

Config.fromJson(Map<String, dynamic> json)
: co19Dir = json["co19Dir"],
specPath = json["specPath"];
}
37 changes: 37 additions & 0 deletions tools/spec_coverage/lib/spec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:io';

import 'package:spec_coverage/spec_parser.dart';

class Spec {
late final List<Chapter> chapters;

Spec.fromTxt(String path) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this could be a static method returning Spec (possibly renamed as Specification). The class could then have a private constructor accepting the list, and chapters wouldn't have to be late.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found another way. Fixed.

File file = File(path);
List<String> lines = file.readAsLinesSync();
SpecParser sp = SpecParser();
chapters = sp.parse(lines);
}
}

class Chapter {
ChapterNumber number;
String header;
late String co19DirName;
List<Chapter> subChapters = [];
List<String> lines = [];

Chapter({required this.number, required this.header})
: co19DirName = header.replaceAll(" ", "_");

@override
String toString() => "$number $header";
}

class ChapterNumber {
final List<int> numbers;

ChapterNumber(this.numbers);

@override
String toString() => numbers.join(".");
}
61 changes: 61 additions & 0 deletions tools/spec_coverage/lib/spec_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'spec.dart';

class SpecParser {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so this class depends in detail on the format of spec.txt. It's a good thing that this dependency is encapsulated to this class alone.

static const pchar = "§";
int chapterCounter1 = 0;
int chapterCounter2 = 0;
int chapterCounter3 = 0;

List<Chapter> parse(List<String> lines) {
return _parseLevel(null, lines, 1);
}

List<Chapter> _parseLevel(Chapter? ch, List<String> lines, int level) {
final List<Chapter> parsed = [];
final List<String> 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 = 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]);
}
}
9 changes: 9 additions & 0 deletions tools/spec_coverage/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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'