|
| 1 | +// Override `flutter test`'s default timeout |
| 2 | +@Timeout(Duration(minutes: 10)) |
| 3 | +library; |
| 4 | + |
| 5 | +import 'dart:io'; |
| 6 | +import 'dart:math'; |
| 7 | + |
| 8 | +import 'package:checks/checks.dart'; |
| 9 | +import 'package:collection/collection.dart'; |
| 10 | +import 'package:flutter/foundation.dart'; |
| 11 | +import 'package:flutter_test/flutter_test.dart'; |
| 12 | +import 'package:zulip/model/content.dart'; |
| 13 | +import 'package:zulip/model/settings.dart'; |
| 14 | + |
| 15 | +import '../../test/model/binding.dart'; |
| 16 | +import 'model.dart'; |
| 17 | + |
| 18 | +void main() async { |
| 19 | + TestZulipBinding.ensureInitialized(); |
| 20 | + await testBinding.globalStore.settings.setBool( |
| 21 | + BoolGlobalSetting.renderKatex, true); |
| 22 | + |
| 23 | + Future<void> checkForKatexFailuresInFile(File file) async { |
| 24 | + int totalMessageCount = 0; |
| 25 | + final Set<int> katexMessageIds = <int>{}; |
| 26 | + final Set<int> failedKatexMessageIds = <int>{}; |
| 27 | + int totalMathBlockNodes = 0; |
| 28 | + int failedMathBlockNodes = 0; |
| 29 | + int totalMathInlineNodes = 0; |
| 30 | + int failedMathInlineNodes = 0; |
| 31 | + |
| 32 | + final failedMessageIdsByReason = <String, Set<int>>{}; |
| 33 | + final failedMathNodesByReason = <String, List<MathNode>>{}; |
| 34 | + |
| 35 | + void walk(int messageId, DiagnosticsNode node) { |
| 36 | + final value = node.value; |
| 37 | + if (value is UnimplementedNode) return; |
| 38 | + |
| 39 | + for (final child in node.getChildren()) { |
| 40 | + walk(messageId, child); |
| 41 | + } |
| 42 | + |
| 43 | + if (value is! MathNode) return; |
| 44 | + katexMessageIds.add(messageId); |
| 45 | + switch (value) { |
| 46 | + case MathBlockNode(): totalMathBlockNodes++; |
| 47 | + case MathInlineNode(): totalMathInlineNodes++; |
| 48 | + } |
| 49 | + |
| 50 | + if (value.nodes != null) return; |
| 51 | + failedKatexMessageIds.add(messageId); |
| 52 | + switch (value) { |
| 53 | + case MathBlockNode(): failedMathBlockNodes++; |
| 54 | + case MathInlineNode(): failedMathInlineNodes++; |
| 55 | + } |
| 56 | + |
| 57 | + final hardFailReason = value.debugHardFailReason; |
| 58 | + final softFailReason = value.debugSoftFailReason; |
| 59 | + int failureCount = 0; |
| 60 | + |
| 61 | + if (hardFailReason != null) { |
| 62 | + final firstLine = hardFailReason.stackTrace.toString().split('\n').first; |
| 63 | + final reason = 'hard fail: ${hardFailReason.error} "$firstLine"'; |
| 64 | + (failedMessageIdsByReason[reason] ??= {}).add(messageId); |
| 65 | + (failedMathNodesByReason[reason] ??= []).add(value); |
| 66 | + failureCount++; |
| 67 | + } |
| 68 | + |
| 69 | + if (softFailReason != null) { |
| 70 | + for (final cssClass in softFailReason.unsupportedCssClasses) { |
| 71 | + final reason = 'unsupported css class: $cssClass'; |
| 72 | + (failedMessageIdsByReason[reason] ??= {}).add(messageId); |
| 73 | + (failedMathNodesByReason[reason] ??= []).add(value); |
| 74 | + failureCount++; |
| 75 | + } |
| 76 | + for (final cssProp in softFailReason.unsupportedInlineCssProperties) { |
| 77 | + final reason = 'unsupported inline css property: $cssProp'; |
| 78 | + (failedMessageIdsByReason[reason] ??= {}).add(messageId); |
| 79 | + (failedMathNodesByReason[reason] ??= []).add(value); |
| 80 | + failureCount++; |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + if (failureCount == 0) { |
| 85 | + final reason = 'unknown'; |
| 86 | + (failedMessageIdsByReason[reason] ??= {}).add(messageId); |
| 87 | + (failedMathNodesByReason[reason] ??= []).add(value); |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + await for (final message in readMessagesFromJsonl(file)) { |
| 92 | + totalMessageCount++; |
| 93 | + walk(message.id, parseContent(message.content).toDiagnosticsNode()); |
| 94 | + } |
| 95 | + |
| 96 | + final buf = StringBuffer(); |
| 97 | + buf.writeln(); |
| 98 | + buf.writeln('Out of $totalMessageCount total messages,' |
| 99 | + ' ${katexMessageIds.length} of them were KaTeX containing messages' |
| 100 | + ' and ${failedKatexMessageIds.length} of them failed.'); |
| 101 | + buf.writeln('There were $totalMathBlockNodes math block nodes out of which $failedMathBlockNodes failed.'); |
| 102 | + buf.writeln('There were $totalMathInlineNodes math inline nodes out of which $failedMathInlineNodes failed.'); |
| 103 | + buf.writeln(); |
| 104 | + |
| 105 | + for (final MapEntry(key: reason, value: messageIds) in failedMessageIdsByReason.entries.sorted( |
| 106 | + (a, b) => b.value.length.compareTo(a.value.length), |
| 107 | + )) { |
| 108 | + final failedMathNodes = failedMathNodesByReason[reason]!; |
| 109 | + int oldestId = messageIds.reduce(min); |
| 110 | + int newestId = messageIds.reduce(max); |
| 111 | + |
| 112 | + buf.writeln('Because of $reason:'); |
| 113 | + buf.writeln(' ${messageIds.length} messages failed.'); |
| 114 | + buf.writeln(' Oldest message: $oldestId, Newest message: $newestId'); |
| 115 | + buf.writeln(' Message IDs (upto 100): ${messageIds.take(100).join(', ')}'); |
| 116 | + buf.writeln(' TeX source (upto 30):'); |
| 117 | + for (final node in failedMathNodes.take(30)) { |
| 118 | + final type = switch (node) { |
| 119 | + MathBlockNode() => 'block', |
| 120 | + MathInlineNode() => 'inline', |
| 121 | + }; |
| 122 | + buf.writeln(' $type: "${node.texSource}"'); |
| 123 | + } |
| 124 | + buf.writeln(' HTML (upto 10):'); |
| 125 | + for (final node in failedMathNodes.take(10)) { |
| 126 | + buf.writeln(' ${node.debugHtmlText}'); |
| 127 | + buf.writeln(); |
| 128 | + } |
| 129 | + buf.writeln(); |
| 130 | + } |
| 131 | + |
| 132 | + check(failedKatexMessageIds.length, because: buf.toString()).equals(0); |
| 133 | + } |
| 134 | + |
| 135 | + final corpusFiles = _getCorpusFiles(); |
| 136 | + |
| 137 | + if (corpusFiles.isEmpty) { |
| 138 | + throw Exception('No corpus found in directory "$_corpusDirPath" to check' |
| 139 | + ' for katex failures.'); |
| 140 | + } |
| 141 | + |
| 142 | + group('Check for katex failures in', () { |
| 143 | + for (final file in corpusFiles) { |
| 144 | + test(file.path, () => checkForKatexFailuresInFile(file)); |
| 145 | + } |
| 146 | + }); |
| 147 | +} |
| 148 | + |
| 149 | +const String _corpusDirPath = String.fromEnvironment('corpusDir'); |
| 150 | + |
| 151 | +Iterable<File> _getCorpusFiles() { |
| 152 | + final corpusDir = Directory(_corpusDirPath); |
| 153 | + return corpusDir.existsSync() ? corpusDir.listSync().whereType<File>() : []; |
| 154 | +} |
0 commit comments