You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
5.7 KiB
166 lines
5.7 KiB
// DateKeys, instants and rounds (lib/src/datekey.dart), as datekey.test.ts
|
|
// of datekeys-ts: properties that hold for every value, on random values of
|
|
// a fixed seed: dk1_ strings that read back, instants that format and parse
|
|
// back to the nanosecond, and rounds whose time is the first at or after
|
|
// the instant. The values and the texts of Go are in the differential
|
|
// (formats_datekey.json and formats_time.json). It reads no file: it runs on
|
|
// the VM and compiled to JavaScript.
|
|
library;
|
|
|
|
import 'dart:math';
|
|
|
|
import 'package:datekeys/src/datekey.dart';
|
|
import 'package:datekeys/src/errors.dart';
|
|
import 'package:datekeys/src/profile.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
String codeOf(void Function() body) {
|
|
try {
|
|
body();
|
|
return 'ok';
|
|
} on DateKeysException catch (e) {
|
|
return e.code.code;
|
|
}
|
|
}
|
|
|
|
// A random int in 0..n-1 for n up to 2^53, from two halves.
|
|
int below(Random r, int n) {
|
|
final hi = r.nextInt(1 << 21);
|
|
final lo = r.nextInt(4294967296);
|
|
return (hi * 4294967296 + lo) % n;
|
|
}
|
|
|
|
void main() {
|
|
final q = quicknet();
|
|
|
|
test('dk1_ strings of valid DateKeys read back, and invalid ones have '
|
|
'none', () {
|
|
final r = Random(7);
|
|
for (var i = 0; i < 300; i++) {
|
|
final d = DateKey(
|
|
['datekeys:quicknet:v1', 'a', '0:._-', 'z' * 128][i % 4],
|
|
1 + below(r, maxDateKeyRound),
|
|
);
|
|
final s = compactDateKey(d);
|
|
expect(s, startsWith(dk1Prefix));
|
|
expect(parseDateKey(s), d);
|
|
expect('$d', s);
|
|
expect(
|
|
canonicalJson(d),
|
|
'{"version":1,"network":"${d.profileId}","round":${d.round}}',
|
|
);
|
|
}
|
|
for (final d in [
|
|
const DateKey('A', 1),
|
|
const DateKey('a', 0),
|
|
const DateKey('a', maxDateKeyRound + 1),
|
|
const DateKey('', 1),
|
|
]) {
|
|
expect([canonicalJson(d), compactDateKey(d)], [null, '']);
|
|
}
|
|
});
|
|
|
|
test('instants order by seconds, then nanoseconds', () {
|
|
Instant t(int s, int ns) => Instant(s, ns);
|
|
expect(compareInstants(t(1, 0), t(1, 0)), 0);
|
|
expect(compareInstants(t(1, 0), t(1, 1)), -1);
|
|
expect(compareInstants(t(1, 999999999), t(2, 0)), -1);
|
|
expect(compareInstants(t(2, 0), t(1, 999999999)), 1);
|
|
expect(compareInstants(t(-5, 3), t(-5, 2)), 1);
|
|
expect(t(3, 4), t(3, 4));
|
|
expect(t(3, 4).hashCode, t(3, 4).hashCode);
|
|
expect([t(2, 0), t(1, 5), t(1, 4)]..sort(), [t(1, 4), t(1, 5), t(2, 0)]);
|
|
expect(() => Instant(0, -1), throwsRangeError);
|
|
expect(() => Instant(0, 1000000000), throwsRangeError);
|
|
});
|
|
|
|
test('instants format and parse back, to the nanosecond', () {
|
|
final r = Random(11);
|
|
// From 0000-01-01 to 9999-12-31, in UTC.
|
|
const first = -62167219200;
|
|
const span = 253402300799 - first + 1;
|
|
for (var i = 0; i < 2000; i++) {
|
|
final t = Instant(
|
|
first + below(r, span),
|
|
[0, 1, 999999999, r.nextInt(1000000000)][i % 4],
|
|
);
|
|
expect(parseRfc3339(formatRfc3339Nano(t)), t);
|
|
final whole = parseRfc3339(formatRfc3339(t));
|
|
expect([whole.seconds, whole.nanos], [t.seconds, 0]);
|
|
expect('$t', formatRfc3339Nano(t));
|
|
}
|
|
expect(() => parseRfc3339('2023-08-23'), throwsFormatException);
|
|
});
|
|
|
|
test('instants convert to and from DateTime', () {
|
|
final t = DateTime.utc(2030, 1, 2, 3, 4, 5, 678);
|
|
expect(
|
|
Instant.fromDateTime(t),
|
|
Instant(t.millisecondsSinceEpoch ~/ 1000, 678000000),
|
|
);
|
|
expect(Instant.fromDateTime(t).toDateTime(), t);
|
|
final before = DateTime.utc(1969, 12, 31, 23, 59, 59, 500);
|
|
expect(Instant.fromDateTime(before), Instant(-1, 500000000));
|
|
expect(Instant(-1, 500000000).toDateTime(), before);
|
|
});
|
|
|
|
test('a long horizon is more than 365 days ahead, to the nanosecond', () {
|
|
final now = Instant(1790000000, 500);
|
|
Instant at(int s, int ns) => Instant(now.seconds + s, ns);
|
|
expect(longHorizonSeconds, 365 * 24 * 60 * 60);
|
|
expect(isLongHorizon(at(longHorizonSeconds, 500), now), isFalse);
|
|
expect(isLongHorizon(at(longHorizonSeconds, 501), now), isTrue);
|
|
expect(isLongHorizon(at(1, 0), now), isFalse);
|
|
});
|
|
|
|
test('the round of an instant is the first whose time is at or after it, '
|
|
'on profiles of other periods too', () {
|
|
final r = Random(13);
|
|
for (final p in [
|
|
q,
|
|
q.copyWith(period: 1),
|
|
q.copyWith(period: 7, genesisTime: 1),
|
|
q.copyWith(period: 86400, genesisTime: 253402300799 - 86400 * 10),
|
|
]) {
|
|
final last = p.maxRound;
|
|
final lastTime = roundTime(p, last).seconds;
|
|
expect(lastTime <= maxUnixTime, isTrue);
|
|
expect(lastTime + p.period > maxUnixTime, isTrue);
|
|
expect(codeOf(() => roundTime(p, last + 1)), 'ERR_DATEKEY_INVALID');
|
|
expect(codeOf(() => roundTime(p, 0)), 'ERR_DATEKEY_INVALID');
|
|
for (var i = 0; i < 300; i++) {
|
|
final at = Instant(
|
|
p.genesisTime + below(r, lastTime - p.genesisTime + 1),
|
|
[0, 0, 1, r.nextInt(1000000000)][i % 4],
|
|
);
|
|
final d = resolveDateKey(p, at);
|
|
final t = roundTime(p, d.round);
|
|
expect(compareInstants(t, at) >= 0, isTrue);
|
|
if (d.round > 1) {
|
|
expect(compareInstants(roundTime(p, d.round - 1), at) < 0, isTrue);
|
|
}
|
|
expect(unlockAt(d, p), t);
|
|
validateDateKey(d, p);
|
|
}
|
|
// Before the genesis and after the last round.
|
|
expect(
|
|
codeOf(() => resolveDateKey(p, Instant(p.genesisTime - 1, 999999999))),
|
|
'ERR_DATEKEY_INVALID',
|
|
);
|
|
expect(
|
|
codeOf(() => resolveDateKey(p, Instant(lastTime, 1))),
|
|
'ERR_DATEKEY_INVALID',
|
|
);
|
|
}
|
|
expect(unlockAt(const DateKey('other', 2), q), isNull);
|
|
expect(
|
|
codeOf(() => validateDateKey(const DateKey('other', 2), q)),
|
|
'ERR_PROFILE_MISMATCH',
|
|
);
|
|
expect(
|
|
codeOf(() => resolveDateKey(q.copyWith(period: 0), Instant(0))),
|
|
'ERR_UNKNOWN_PROFILE',
|
|
);
|
|
});
|
|
}
|