mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
crdt_sync_dart: latest stable + exact pins, 100% coverage, CI for both crdt_sync packages
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
723ac95443
commit
922fb36208
48
.github/workflows/crdt-sync-dart-tests.yml
vendored
Normal file
48
.github/workflows/crdt-sync-dart-tests.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: crdt_sync_dart tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['crdt_sync_dart/**']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['crdt_sync_dart/**']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: crdt_sync_dart
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Pure Dart, no Flutter dependency -- so setup-dart, not flutter-action.
|
||||||
|
- name: Set up Dart
|
||||||
|
uses: dart-lang/setup-dart@v1
|
||||||
|
with:
|
||||||
|
sdk: stable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dart pub get
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: dart analyze --fatal-infos
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: dart test --coverage=coverage
|
||||||
|
|
||||||
|
# Unlike `flutter test --coverage`, `dart test --coverage` emits raw VM
|
||||||
|
# JSON and no lcov. Skipping this step leaves a stale lcov.info on disk
|
||||||
|
# that the gate below happily "passes" -- which is exactly how this
|
||||||
|
# package drifted to 96% while believing it was at 100%.
|
||||||
|
- name: Convert coverage to lcov
|
||||||
|
run: |
|
||||||
|
dart pub global activate coverage
|
||||||
|
dart pub global run coverage:format_coverage \
|
||||||
|
--lcov --in=coverage/test --out=coverage/lcov.info \
|
||||||
|
--report-on=lib --packages=.dart_tool/package_config.json
|
||||||
|
|
||||||
|
- name: Enforce 100% line coverage
|
||||||
|
run: scripts/check_coverage.sh coverage/lcov.info 100
|
||||||
36
.github/workflows/crdt-sync-flutter-tests.yml
vendored
Normal file
36
.github/workflows/crdt-sync-flutter-tests.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: crdt_sync_flutter tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['crdt_sync_flutter/**', 'crdt_sync_dart/**']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['crdt_sync_flutter/**', 'crdt_sync_dart/**']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: crdt_sync_flutter
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: flutter analyze
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: flutter test --coverage
|
||||||
|
|
||||||
|
- name: Enforce 100% line coverage
|
||||||
|
run: scripts/check_coverage.sh coverage/lcov.info 100
|
||||||
@ -9,9 +9,9 @@ environment:
|
|||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
crypto: ^3.0.7
|
crypto: 3.0.7
|
||||||
http: ^1.6.0
|
http: 1.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^5.0.0
|
lints: 6.1.0
|
||||||
test: ^1.25.0
|
test: 1.31.2
|
||||||
|
|||||||
32
crdt_sync_dart/scripts/check_coverage.sh
Executable file
32
crdt_sync_dart/scripts/check_coverage.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
|
#
|
||||||
|
# Thin delegate to the shared gate in ~/utils/scripts, which owns the parsing
|
||||||
|
# and the threshold comparison. This file used to be a byte-identical copy of
|
||||||
|
# that logic; three such copies existed, which is how one package's idea of
|
||||||
|
# "covered" drifts from every other package's.
|
||||||
|
#
|
||||||
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Repo-relative, resolved from this script's own location: an absolute
|
||||||
|
# /home/... path exits 127 on an Actions runner.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
readonly SHARED_GATE="${SCRIPT_DIR}/../../scripts/check_coverage.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ ! -f "$SHARED_GATE" ]]; then
|
||||||
|
echo "Error: shared coverage gate not found at $SHARED_GATE" >&2
|
||||||
|
echo " It lives in the utils repo at scripts/check_coverage.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$SHARED_GATE" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
132
crdt_sync_dart/test/default_client_and_io_test.dart
Normal file
132
crdt_sync_dart/test/default_client_and_io_test.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crdt_sync/crdt_sync.dart';
|
||||||
|
import 'package:crdt_sync/crdt_sync_io.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/testing.dart' as http_testing;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
/// An in-memory [LogPersistence] so store tests need no filesystem.
|
||||||
|
class _MemoryPersistence implements LogPersistence {
|
||||||
|
_MemoryPersistence([this._contents]);
|
||||||
|
|
||||||
|
String? _contents;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read() async => _contents;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> write(String contents) async => _contents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('default http client', () {
|
||||||
|
// These constructors fall back to a real http.Client() when none is
|
||||||
|
// injected. Nothing here issues a request -- the point is only that the
|
||||||
|
// default branch is taken and the resulting client closes cleanly.
|
||||||
|
test('GitHubClient builds one when none is injected', () {
|
||||||
|
final client = GitHubClient(owner: 'o', repo: 'r', token: 't');
|
||||||
|
addTearDown(client.close);
|
||||||
|
expect(client.owner, 'o');
|
||||||
|
expect(client.repo, 'r');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GitHubClient.close is safe to call', () {
|
||||||
|
GitHubClient(owner: 'o', repo: 'r', token: 't').close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accountFromWrapper builds and closes its own client', () async {
|
||||||
|
// Nothing listens on this port, so the GET throws and the `on Exception`
|
||||||
|
// path returns null -- while the `finally` still closes the client the
|
||||||
|
// function created for itself.
|
||||||
|
final account = await accountFromWrapper(
|
||||||
|
Uri.parse('http://127.0.0.1:1'),
|
||||||
|
);
|
||||||
|
expect(account, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accountFromWrapper leaves an injected client open', () async {
|
||||||
|
final mock = http_testing.MockClient(
|
||||||
|
(_) async => http.Response(jsonEncode({}), 404),
|
||||||
|
);
|
||||||
|
expect(await accountFromWrapper(Uri.parse('http://x'), client: mock),
|
||||||
|
isNull);
|
||||||
|
// Still usable: the function must not close a client it did not create.
|
||||||
|
expect((await mock.get(Uri.parse('http://x'))).statusCode, 404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('RemoteNotFoundError', () {
|
||||||
|
test('carries its message and is a RemoteSyncError', () {
|
||||||
|
final error = RemoteNotFoundError('nothing synced yet');
|
||||||
|
expect(error, isA<RemoteSyncError>());
|
||||||
|
expect(error.toString(), contains('nothing synced yet'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LogStore tombstone clock tracking', () {
|
||||||
|
test('a loaded tombstone advances the clock past its deletedHlc', () async {
|
||||||
|
// deletedHlc is far ahead of any field clock, so the next local write
|
||||||
|
// can only outrank it if load() fed the tombstone's clock in too.
|
||||||
|
final deletedHlc = Hlc(
|
||||||
|
wallTimeMs: DateTime.utc(2030).millisecondsSinceEpoch,
|
||||||
|
counter: 7,
|
||||||
|
nodeId: 'node-remote',
|
||||||
|
);
|
||||||
|
final log = {
|
||||||
|
'r1': Record(
|
||||||
|
id: 'r1',
|
||||||
|
fields: {
|
||||||
|
'title': ('old', Hlc(wallTimeMs: 1, counter: 0, nodeId: 'node-a')),
|
||||||
|
},
|
||||||
|
deleted: true,
|
||||||
|
deletedHlc: deletedHlc,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
final store = LogStore(
|
||||||
|
persistence: _MemoryPersistence(logToJson(log)),
|
||||||
|
nodeId: 'node-a',
|
||||||
|
);
|
||||||
|
addTearDown(store.close);
|
||||||
|
await store.load();
|
||||||
|
|
||||||
|
expect(store.nextHlc() > deletedHlc, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('FileLogPersistence', () {
|
||||||
|
late Directory dir;
|
||||||
|
|
||||||
|
setUp(() => dir = Directory.systemTemp.createTempSync('crdt_sync_test'));
|
||||||
|
tearDown(() => dir.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
test('read returns null when the file does not exist', () async {
|
||||||
|
final store = FileLogPersistence(File('${dir.path}/absent.json'));
|
||||||
|
expect(await store.read(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('read returns null when the file cannot be read', () async {
|
||||||
|
// A mode-000 file still passes existsSync, so the guard does not fire
|
||||||
|
// and readAsString throws PathAccessException (a FileSystemException).
|
||||||
|
// A directory would NOT work here: File.existsSync() is false for one,
|
||||||
|
// so read() would return early down the guard path instead.
|
||||||
|
final file = File('${dir.path}/unreadable.json')..writeAsStringSync('{}');
|
||||||
|
final chmod = Process.runSync('chmod', ['000', file.path]);
|
||||||
|
expect(chmod.exitCode, 0);
|
||||||
|
addTearDown(() => Process.runSync('chmod', ['644', file.path]));
|
||||||
|
|
||||||
|
expect(await FileLogPersistence(file).read(), isNull);
|
||||||
|
},
|
||||||
|
// root ignores the permission bits, so the exception never fires.
|
||||||
|
skip: Platform.environment['USER'] == 'root'
|
||||||
|
? 'permission bits do not apply to root'
|
||||||
|
: null);
|
||||||
|
|
||||||
|
test('round-trips what it wrote', () async {
|
||||||
|
final store = FileLogPersistence(File('${dir.path}/log.json'));
|
||||||
|
await store.write('{"hello":"world"}');
|
||||||
|
expect(await store.read(), '{"hello":"world"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
89
crdt_sync_dart/test/hlc_value_test.dart
Normal file
89
crdt_sync_dart/test/hlc_value_test.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:crdt_sync/crdt_sync.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
Hlc _make(int wallTimeMs, {int counter = 0, String nodeId = 'node-a'}) =>
|
||||||
|
Hlc(wallTimeMs: wallTimeMs, counter: counter, nodeId: nodeId);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('comparison operators', () {
|
||||||
|
// compareTo is exercised elsewhere; these cover the four operators that
|
||||||
|
// wrap it, each of which has its own boundary at "exactly equal".
|
||||||
|
test('> is false for equal clocks', () {
|
||||||
|
expect(_make(100) > _make(100), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('>= is true for equal clocks', () {
|
||||||
|
expect(_make(100) >= _make(100), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('>= is true for a greater clock', () {
|
||||||
|
expect(_make(200) >= _make(100), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('>= is false for a lesser clock', () {
|
||||||
|
expect(_make(100) >= _make(200), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('< is false for equal clocks', () {
|
||||||
|
expect(_make(100) < _make(100), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('< is true for a lesser clock', () {
|
||||||
|
expect(_make(100) < _make(200), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('<= is true for equal clocks', () {
|
||||||
|
expect(_make(100) <= _make(100), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('<= is false for a greater clock', () {
|
||||||
|
expect(_make(200) <= _make(100), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('equality', () {
|
||||||
|
test('a non-Hlc is never equal', () {
|
||||||
|
// ignore: unrelated_type_equality_checks
|
||||||
|
expect(_make(100) == 'not an hlc', isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('differing wall time is not equal', () {
|
||||||
|
expect(_make(100), isNot(equals(_make(200))));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('differing counter is not equal', () {
|
||||||
|
expect(_make(100), isNot(equals(_make(100, counter: 1))));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('differing node id is not equal', () {
|
||||||
|
expect(_make(100), isNot(equals(_make(100, nodeId: 'node-b'))));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('hashCode', () {
|
||||||
|
test('equal clocks hash equally', () {
|
||||||
|
expect(_make(100, counter: 2).hashCode, _make(100, counter: 2).hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing field changes the hash', () {
|
||||||
|
expect(
|
||||||
|
_make(100, counter: 2).hashCode,
|
||||||
|
isNot(_make(100, counter: 3).hashCode),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clocks survive a round trip through a Set', () {
|
||||||
|
final set = {_make(100), _make(100), _make(200)};
|
||||||
|
expect(set, hasLength(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('toString', () {
|
||||||
|
test('names every field so a failed expect is readable', () {
|
||||||
|
expect(
|
||||||
|
_make(100, counter: 2, nodeId: 'node-z').toString(),
|
||||||
|
'Hlc(wallTimeMs: 100, counter: 2, nodeId: node-z)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
130
crdt_sync_dart/test/record_value_test.dart
Normal file
130
crdt_sync_dart/test/record_value_test.dart
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:crdt_sync/crdt_sync.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
Hlc _make(int wallTimeMs, {int counter = 0, String nodeId = 'node-a'}) =>
|
||||||
|
Hlc(wallTimeMs: wallTimeMs, counter: counter, nodeId: nodeId);
|
||||||
|
|
||||||
|
Record _record({
|
||||||
|
String id = 'r1',
|
||||||
|
Map<String, Field>? fields,
|
||||||
|
bool deleted = false,
|
||||||
|
Hlc? deletedHlc,
|
||||||
|
}) => Record(
|
||||||
|
id: id,
|
||||||
|
fields: fields ?? {'title': ('hello', _make(100))},
|
||||||
|
deleted: deleted,
|
||||||
|
deletedHlc: deletedHlc,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('equality', () {
|
||||||
|
test('a non-Record is never equal', () {
|
||||||
|
// ignore: unrelated_type_equality_checks
|
||||||
|
expect(_record() == 'not a record', isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same id, fields and tombstone are equal', () {
|
||||||
|
expect(_record(), equals(_record()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing id is not equal', () {
|
||||||
|
expect(_record(), isNot(equals(_record(id: 'r2'))));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing tombstone is not equal', () {
|
||||||
|
expect(_record(), isNot(equals(_record(deleted: true))));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing deletedHlc is not equal', () {
|
||||||
|
expect(
|
||||||
|
_record(deleted: true, deletedHlc: _make(100)),
|
||||||
|
isNot(equals(_record(deleted: true, deletedHlc: _make(200)))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('field comparison', () {
|
||||||
|
// _fieldsEqual is the one part of == that cannot short-circuit on a
|
||||||
|
// scalar, so each of its exits gets its own case.
|
||||||
|
test('a differing field count is not equal', () {
|
||||||
|
expect(
|
||||||
|
_record(),
|
||||||
|
isNot(
|
||||||
|
equals(
|
||||||
|
_record(
|
||||||
|
fields: {'title': ('hello', _make(100)), 'body': ('x', _make(1))},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing field value is not equal', () {
|
||||||
|
expect(
|
||||||
|
_record(),
|
||||||
|
isNot(equals(_record(fields: {'title': ('goodbye', _make(100))}))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing field clock is not equal', () {
|
||||||
|
expect(
|
||||||
|
_record(),
|
||||||
|
isNot(equals(_record(fields: {'title': ('hello', _make(200))}))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a renamed field of the same count is not equal', () {
|
||||||
|
expect(
|
||||||
|
_record(),
|
||||||
|
isNot(equals(_record(fields: {'heading': ('hello', _make(100))}))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field order does not affect equality', () {
|
||||||
|
final a = _record(
|
||||||
|
fields: {'a': ('1', _make(100)), 'b': ('2', _make(200))},
|
||||||
|
);
|
||||||
|
final b = _record(
|
||||||
|
fields: {'b': ('2', _make(200)), 'a': ('1', _make(100))},
|
||||||
|
);
|
||||||
|
expect(a, equals(b));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an empty field map equals another empty one', () {
|
||||||
|
expect(_record(fields: {}), equals(_record(fields: {})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('hashCode', () {
|
||||||
|
test('equal records hash equally', () {
|
||||||
|
expect(_record().hashCode, _record().hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a differing id changes the hash', () {
|
||||||
|
expect(_record().hashCode, isNot(_record(id: 'r2').hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hashing does not depend on field order', () {
|
||||||
|
final a = _record(
|
||||||
|
fields: {'a': ('1', _make(100)), 'b': ('2', _make(200))},
|
||||||
|
);
|
||||||
|
final b = _record(
|
||||||
|
fields: {'b': ('2', _make(200)), 'a': ('1', _make(100))},
|
||||||
|
);
|
||||||
|
expect(a.hashCode, b.hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('records survive a round trip through a Set', () {
|
||||||
|
expect({_record(), _record(), _record(id: 'r2')}, hasLength(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('toString', () {
|
||||||
|
test('names every field so a failed expect is readable', () {
|
||||||
|
expect(
|
||||||
|
_record(fields: {}).toString(),
|
||||||
|
'Record(id: r1, fields: {}, deleted: false, deletedHlc: null)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
32
crdt_sync_flutter/scripts/check_coverage.sh
Executable file
32
crdt_sync_flutter/scripts/check_coverage.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
|
#
|
||||||
|
# Thin delegate to the shared gate in ~/utils/scripts, which owns the parsing
|
||||||
|
# and the threshold comparison. This file used to be a byte-identical copy of
|
||||||
|
# that logic; three such copies existed, which is how one package's idea of
|
||||||
|
# "covered" drifts from every other package's.
|
||||||
|
#
|
||||||
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Repo-relative, resolved from this script's own location: an absolute
|
||||||
|
# /home/... path exits 127 on an Actions runner.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
readonly SHARED_GATE="${SCRIPT_DIR}/../../scripts/check_coverage.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ ! -f "$SHARED_GATE" ]]; then
|
||||||
|
echo "Error: shared coverage gate not found at $SHARED_GATE" >&2
|
||||||
|
echo " It lives in the utils repo at scripts/check_coverage.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$SHARED_GATE" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
54
crdt_sync_flutter/test/descriptor_value_test.dart
Normal file
54
crdt_sync_flutter/test/descriptor_value_test.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:crdt_sync/crdt_sync.dart';
|
||||||
|
import 'package:crdt_sync_flutter/crdt_sync_flutter.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Built at runtime, not as a top-level `const`.
|
||||||
|
///
|
||||||
|
/// Other suites describe their app with a compile-time `const SyncApp(...)`,
|
||||||
|
/// which the VM folds into a constant before any test body runs -- so the
|
||||||
|
/// constructors never register as executed and read as uncovered. These
|
||||||
|
/// helpers force real invocation.
|
||||||
|
SyncApp _app({WrapperRoutes? routes}) => SyncApp(
|
||||||
|
project: const FirebaseProject(
|
||||||
|
apiKey: 'test-key',
|
||||||
|
databaseUrl: 'https://example-rtdb.europe-west1.firebasedatabase.app',
|
||||||
|
),
|
||||||
|
expectedUid: 'uid-123',
|
||||||
|
routes: routes,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SyncApp', () {
|
||||||
|
test('keeps the project and uid it was built with', () {
|
||||||
|
final app = _app();
|
||||||
|
expect(app.project.apiKey, 'test-key');
|
||||||
|
expect(app.expectedUid, 'uid-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('routes default to null for an app with no desktop wrapper', () {
|
||||||
|
expect(_app().routes, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the routes it was given', () {
|
||||||
|
final routes = WrapperRoutes(credentialsPath: '/sync-credentials');
|
||||||
|
expect(_app(routes: routes).routes, same(routes));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('WrapperRoutes', () {
|
||||||
|
test('accountPath defaults to null', () {
|
||||||
|
final routes = WrapperRoutes(credentialsPath: '/sync-credentials');
|
||||||
|
expect(routes.credentialsPath, '/sync-credentials');
|
||||||
|
expect(routes.accountPath, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps both paths when the legacy one is supplied', () {
|
||||||
|
final routes = WrapperRoutes(
|
||||||
|
credentialsPath: '/sync-credentials',
|
||||||
|
accountPath: '/sync-account',
|
||||||
|
);
|
||||||
|
expect(routes.credentialsPath, '/sync-credentials');
|
||||||
|
expect(routes.accountPath, '/sync-account');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -3,61 +3,30 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Fail if lcov line coverage drops below a threshold.
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
#
|
#
|
||||||
# No Dart package in this repo has coverage enforced in CI yet -- crdt_sync_dart
|
# Thin delegate to the shared gate in ~/utils/scripts, which owns the parsing
|
||||||
# sits at 100% by discipline, not a gate. This script is that gate for
|
# and the threshold comparison. This file used to be a byte-identical copy of
|
||||||
# sync_settings_ui, which ships as a 100%-covered shared UI package by
|
# that logic; three such copies existed, which is how one package's idea of
|
||||||
# requirement, not by convention.
|
# "covered" drifts from every other package's.
|
||||||
#
|
#
|
||||||
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
# Repo-relative, resolved from this script's own location: an absolute
|
||||||
readonly SCRIPT_NAME
|
# /home/... path exits 127 on an Actions runner.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
readonly SHARED_GATE="${SCRIPT_DIR}/../../scripts/check_coverage.sh"
|
||||||
|
|
||||||
usage() {
|
main() {
|
||||||
echo "Usage: $SCRIPT_NAME <lcov.info> <min_percent>"
|
if [[ ! -f "$SHARED_GATE" ]]; then
|
||||||
echo " Fails when line coverage in lcov.info is below min_percent."
|
echo "Error: shared coverage gate not found at $SHARED_GATE" >&2
|
||||||
exit 0
|
echo " It lives in the utils repo at scripts/check_coverage.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$SHARED_GATE" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
main "$@"
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $# -ne 2 ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
lcov_file="$1"
|
|
||||||
min_percent="$2"
|
|
||||||
|
|
||||||
if [[ ! -f "$lcov_file" ]]; then
|
|
||||||
echo "Error: $lcov_file not found. Run 'flutter test --coverage' first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sum LH (lines hit) and LF (lines found) across every SF (source file)
|
|
||||||
# block rather than trusting a single top-level total -- lcov.info has no
|
|
||||||
# such total line, only per-file LH/LF pairs.
|
|
||||||
read -r hit found <<< "$(awk '
|
|
||||||
/^LH:/ { hit += substr($0, 4) }
|
|
||||||
/^LF:/ { found += substr($0, 4) }
|
|
||||||
END { print hit, found }
|
|
||||||
' "$lcov_file")"
|
|
||||||
|
|
||||||
if [[ "$found" -eq 0 ]]; then
|
|
||||||
echo "Error: $lcov_file reports zero coverable lines." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
percent=$(awk -v h="$hit" -v f="$found" 'BEGIN { printf "%.2f", (h / f) * 100 }')
|
|
||||||
meets_threshold=$(awk -v p="$percent" -v m="$min_percent" 'BEGIN { print (p + 0 >= m + 0) ? 1 : 0 }')
|
|
||||||
|
|
||||||
echo "Line coverage: $hit/$found ($percent%), threshold: $min_percent%"
|
|
||||||
|
|
||||||
if [[ "$meets_threshold" -ne 1 ]]; then
|
|
||||||
echo "Error: coverage $percent% is below the required $min_percent%." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
@ -3,61 +3,30 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Fail if lcov line coverage drops below a threshold.
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
#
|
#
|
||||||
# No Dart package in this repo has coverage enforced in CI yet -- crdt_sync_dart
|
# Thin delegate to the shared gate in ~/utils/scripts, which owns the parsing
|
||||||
# sits at 100% by discipline, not a gate. This script is that gate for
|
# and the threshold comparison. This file used to be a byte-identical copy of
|
||||||
# sync_settings_ui, which ships as a 100%-covered shared UI package by
|
# that logic; three such copies existed, which is how one package's idea of
|
||||||
# requirement, not by convention.
|
# "covered" drifts from every other package's.
|
||||||
#
|
#
|
||||||
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
# Repo-relative, resolved from this script's own location: an absolute
|
||||||
readonly SCRIPT_NAME
|
# /home/... path exits 127 on an Actions runner.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
readonly SHARED_GATE="${SCRIPT_DIR}/../../scripts/check_coverage.sh"
|
||||||
|
|
||||||
usage() {
|
main() {
|
||||||
echo "Usage: $SCRIPT_NAME <lcov.info> <min_percent>"
|
if [[ ! -f "$SHARED_GATE" ]]; then
|
||||||
echo " Fails when line coverage in lcov.info is below min_percent."
|
echo "Error: shared coverage gate not found at $SHARED_GATE" >&2
|
||||||
exit 0
|
echo " It lives in the utils repo at scripts/check_coverage.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$SHARED_GATE" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
main "$@"
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $# -ne 2 ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
lcov_file="$1"
|
|
||||||
min_percent="$2"
|
|
||||||
|
|
||||||
if [[ ! -f "$lcov_file" ]]; then
|
|
||||||
echo "Error: $lcov_file not found. Run 'flutter test --coverage' first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sum LH (lines hit) and LF (lines found) across every SF (source file)
|
|
||||||
# block rather than trusting a single top-level total -- lcov.info has no
|
|
||||||
# such total line, only per-file LH/LF pairs.
|
|
||||||
read -r hit found <<< "$(awk '
|
|
||||||
/^LH:/ { hit += substr($0, 4) }
|
|
||||||
/^LF:/ { found += substr($0, 4) }
|
|
||||||
END { print hit, found }
|
|
||||||
' "$lcov_file")"
|
|
||||||
|
|
||||||
if [[ "$found" -eq 0 ]]; then
|
|
||||||
echo "Error: $lcov_file reports zero coverable lines." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
percent=$(awk -v h="$hit" -v f="$found" 'BEGIN { printf "%.2f", (h / f) * 100 }')
|
|
||||||
meets_threshold=$(awk -v p="$percent" -v m="$min_percent" 'BEGIN { print (p + 0 >= m + 0) ? 1 : 0 }')
|
|
||||||
|
|
||||||
echo "Line coverage: $hit/$found ($percent%), threshold: $min_percent%"
|
|
||||||
|
|
||||||
if [[ "$meets_threshold" -ne 1 ]]; then
|
|
||||||
echo "Error: coverage $percent% is below the required $min_percent%." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
70
scripts/check_coverage.sh
Executable file
70
scripts/check_coverage.sh
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
|
#
|
||||||
|
# THE shared coverage gate for every package in this repo. Three byte-identical
|
||||||
|
# copies of this file used to live under design_system/, github_device_auth/
|
||||||
|
# and sync_settings_ui/scripts/ -- each claiming in its own header to exist for
|
||||||
|
# one particular package. Those are now thin delegates to this file, for the
|
||||||
|
# same reason check_file_length.sh is shared: a per-package copy is how one
|
||||||
|
# package's idea of "covered" drifts from every other package's.
|
||||||
|
#
|
||||||
|
# Note for pure-Dart packages: `dart test --coverage` writes raw VM JSON, NOT
|
||||||
|
# lcov. Run coverage:format_coverage before this gate, or it will happily pass
|
||||||
|
# against a stale lcov.info from a previous run. crdt_sync_dart really did sit
|
||||||
|
# at 96% for three weeks that way while believing it was at 100%.
|
||||||
|
#
|
||||||
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_NAME="$(basename "$0")"
|
||||||
|
readonly SCRIPT_NAME
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $SCRIPT_NAME <lcov.info> <min_percent>"
|
||||||
|
echo " Fails when line coverage in lcov.info is below min_percent."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
lcov_file="$1"
|
||||||
|
min_percent="$2"
|
||||||
|
|
||||||
|
if [[ ! -f "$lcov_file" ]]; then
|
||||||
|
echo "Error: $lcov_file not found. Run 'flutter test --coverage' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sum LH (lines hit) and LF (lines found) across every SF (source file)
|
||||||
|
# block rather than trusting a single top-level total -- lcov.info has no
|
||||||
|
# such total line, only per-file LH/LF pairs.
|
||||||
|
read -r hit found <<< "$(awk '
|
||||||
|
/^LH:/ { hit += substr($0, 4) }
|
||||||
|
/^LF:/ { found += substr($0, 4) }
|
||||||
|
END { print hit, found }
|
||||||
|
' "$lcov_file")"
|
||||||
|
|
||||||
|
if [[ "$found" -eq 0 ]]; then
|
||||||
|
echo "Error: $lcov_file reports zero coverable lines." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
percent=$(awk -v h="$hit" -v f="$found" 'BEGIN { printf "%.2f", (h / f) * 100 }')
|
||||||
|
meets_threshold=$(awk -v p="$percent" -v m="$min_percent" 'BEGIN { print (p + 0 >= m + 0) ? 1 : 0 }')
|
||||||
|
|
||||||
|
echo "Line coverage: $hit/$found ($percent%), threshold: $min_percent%"
|
||||||
|
|
||||||
|
if [[ "$meets_threshold" -ne 1 ]]; then
|
||||||
|
echo "Error: coverage $percent% is below the required $min_percent%." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@ -3,61 +3,30 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Fail if lcov line coverage drops below a threshold.
|
# Fail if lcov line coverage drops below a threshold.
|
||||||
#
|
#
|
||||||
# No Dart package in this repo has coverage enforced in CI yet -- crdt_sync_dart
|
# Thin delegate to the shared gate in ~/utils/scripts, which owns the parsing
|
||||||
# sits at 100% by discipline, not a gate. This script is that gate for
|
# and the threshold comparison. This file used to be a byte-identical copy of
|
||||||
# sync_settings_ui, which ships as a 100%-covered shared UI package by
|
# that logic; three such copies existed, which is how one package's idea of
|
||||||
# requirement, not by convention.
|
# "covered" drifts from every other package's.
|
||||||
#
|
#
|
||||||
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
# Usage: check_coverage.sh <lcov.info> <min_percent>
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
# Repo-relative, resolved from this script's own location: an absolute
|
||||||
readonly SCRIPT_NAME
|
# /home/... path exits 127 on an Actions runner.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
readonly SHARED_GATE="${SCRIPT_DIR}/../../scripts/check_coverage.sh"
|
||||||
|
|
||||||
usage() {
|
main() {
|
||||||
echo "Usage: $SCRIPT_NAME <lcov.info> <min_percent>"
|
if [[ ! -f "$SHARED_GATE" ]]; then
|
||||||
echo " Fails when line coverage in lcov.info is below min_percent."
|
echo "Error: shared coverage gate not found at $SHARED_GATE" >&2
|
||||||
exit 0
|
echo " It lives in the utils repo at scripts/check_coverage.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$SHARED_GATE" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
main "$@"
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $# -ne 2 ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
lcov_file="$1"
|
|
||||||
min_percent="$2"
|
|
||||||
|
|
||||||
if [[ ! -f "$lcov_file" ]]; then
|
|
||||||
echo "Error: $lcov_file not found. Run 'flutter test --coverage' first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sum LH (lines hit) and LF (lines found) across every SF (source file)
|
|
||||||
# block rather than trusting a single top-level total -- lcov.info has no
|
|
||||||
# such total line, only per-file LH/LF pairs.
|
|
||||||
read -r hit found <<< "$(awk '
|
|
||||||
/^LH:/ { hit += substr($0, 4) }
|
|
||||||
/^LF:/ { found += substr($0, 4) }
|
|
||||||
END { print hit, found }
|
|
||||||
' "$lcov_file")"
|
|
||||||
|
|
||||||
if [[ "$found" -eq 0 ]]; then
|
|
||||||
echo "Error: $lcov_file reports zero coverable lines." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
percent=$(awk -v h="$hit" -v f="$found" 'BEGIN { printf "%.2f", (h / f) * 100 }')
|
|
||||||
meets_threshold=$(awk -v p="$percent" -v m="$min_percent" 'BEGIN { print (p + 0 >= m + 0) ? 1 : 0 }')
|
|
||||||
|
|
||||||
echo "Line coverage: $hit/$found ($percent%), threshold: $min_percent%"
|
|
||||||
|
|
||||||
if [[ "$meets_threshold" -ne 1 ]]; then
|
|
||||||
echo "Error: coverage $percent% is below the required $min_percent%." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user