[vm/shared] Prohibit capturing of 'late final' variables by isolategroup-bound closures.

TEST=run_isolate_group_run_test
BUG=https://github.com/dart-lang/sdk/issues/62181

Change-Id: I50037ede337367020176262b98d2c2fd100b050a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/466820
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
This commit is contained in:
Alexander Aprelev
2025-12-10 08:29:57 -08:00
committed by Commit Queue
parent 420ac4d329
commit 170df25b66
12 changed files with 327 additions and 210 deletions
+1 -1
View File
@@ -681,7 +681,7 @@ type ClosureDeclaration {
type ClosureCode {
UInt flags = (hasExceptionsTable, hasSourcePositions, hasLocalVariables,
capturesOnlyFinalAndSharedVars)
capturesOnlyFinalNotLateVars)
UInt bytecodeSizeInBytes;
Byte[bytecodeSizeInBytes] bytecodes;
@@ -1671,7 +1671,7 @@ class BytecodeGenerator extends RecursiveVisitor {
currentLoopDepth = 0;
savedMaxSourcePositions = <int>[];
locals = new LocalVariables(pragmaParser, node, options, staticTypeContext);
locals = new LocalVariables(node, options, staticTypeContext);
locals.enterScope(node);
final int startPosition;
@@ -2508,14 +2508,14 @@ class BytecodeGenerator extends RecursiveVisitor {
currentLoopDepth = savedLoopDepth;
asyncTryBlock = savedAsyncTryBlock;
bool capturesOnlyFinalAndSharedVars =
locals.capturesOnlyFinalAndSharedVars;
bool capturesOnlyFinalNotLateVars =
locals.capturesOnlyFinalNotLateVars;
locals.leaveScope();
closure.code = new ClosureCode(asm.bytecode, asm.exceptionsTable,
finalizeSourcePositions(), finalizeLocalVariables(),
capturesOnlyFinalAndSharedVars);
capturesOnlyFinalNotLateVars);
_popAssemblerState();
+7 -7
View File
@@ -1162,13 +1162,13 @@ class ClosureCode {
static const hasExceptionsTableFlag = 1 << 0;
static const hasSourcePositionsFlag = 1 << 1;
static const hasLocalVariablesFlag = 1 << 2;
static const capturesOnlyFinalAndSharedVarsFlag = 1 << 3;
static const capturesOnlyFinalNotLateVarsFlag = 1 << 3;
final Uint8List bytecodes;
final ExceptionsTable exceptionsTable;
final SourcePositions? sourcePositions;
final LocalVariableTable? localVariables;
final bool capturesOnlyFinalAndSharedVars;
final bool capturesOnlyFinalNotLateVars;
bool get hasExceptionsTable => exceptionsTable.blocks.isNotEmpty;
bool get hasSourcePositions => sourcePositions?.isNotEmpty ?? false;
@@ -1178,10 +1178,10 @@ class ClosureCode {
(hasExceptionsTable ? hasExceptionsTableFlag : 0) |
(hasSourcePositions ? hasSourcePositionsFlag : 0) |
(hasLocalVariables ? hasLocalVariablesFlag : 0) |
(capturesOnlyFinalAndSharedVars ? capturesOnlyFinalAndSharedVarsFlag : 0);
(capturesOnlyFinalNotLateVars ? capturesOnlyFinalNotLateVarsFlag : 0);
ClosureCode(this.bytecodes, this.exceptionsTable, this.sourcePositions,
this.localVariables, this.capturesOnlyFinalAndSharedVars);
this.localVariables, this.capturesOnlyFinalNotLateVars);
void write(BufferedWriter writer) {
writer.writePackedUInt30(flags);
@@ -1209,12 +1209,12 @@ class ClosureCode {
final localVariables = ((flags & hasLocalVariablesFlag) != 0)
? reader.readLinkOffset<LocalVariableTable>()
: null;
final capturesOnlyFinalAndSharedVars =
(flags & capturesOnlyFinalAndSharedVarsFlag) != 0;
final capturesOnlyFinalNotLateVars =
(flags & capturesOnlyFinalNotLateVarsFlag) != 0;
return new ClosureCode(
bytecodes, exceptionsTable, sourcePositions, localVariables,
capturesOnlyFinalAndSharedVars);
capturesOnlyFinalNotLateVars);
}
@override
+11 -19
View File
@@ -7,8 +7,6 @@ import 'dart:math' show min, max;
import 'package:kernel/ast.dart';
import 'package:kernel/type_environment.dart';
import 'package:vm/transformations/pragma.dart';
import 'dbc.dart';
import 'options.dart' show BytecodeOptions;
@@ -159,8 +157,8 @@ class LocalVariables {
List<VariableDeclaration> get sortedNamedParameters =>
_currentFrame.sortedNamedParameters;
LocalVariables(PragmaAnnotationParser pragmaParser, Member node, this.options, this.staticTypeContext) {
final scopeBuilder = new _ScopeBuilder(pragmaParser, this);
LocalVariables(Member node, this.options, this.staticTypeContext) {
final scopeBuilder = new _ScopeBuilder(this);
node.accept(scopeBuilder);
final allocator = new _Allocator(this);
@@ -177,8 +175,8 @@ class LocalVariables {
_currentFrameInternal = _currentScopeInternal?.frame;
}
bool get capturesOnlyFinalAndSharedVars =>
_currentFrame.capturesOnlyFinalAndSharedVars;
bool get capturesOnlyFinalNotLateVars =>
_currentFrame.capturesOnlyFinalNotLateVars;
void withTemp(TreeNode node, int temp, void action()) {
final old = _temps![node];
@@ -197,18 +195,17 @@ class VarDesc {
final VariableDeclaration declaration;
Scope scope;
bool isCaptured = false;
bool _isShared;
int? index;
int? originalParamSlotIndex;
VarDesc(this.declaration, this.scope, this._isShared) {
VarDesc(this.declaration, this.scope) {
scope.vars.add(this);
}
Frame get frame => scope.frame;
bool get isAllocated => index != null;
bool get isFinalOrShared => declaration.isFinal || _isShared;
bool get isFinalNotLate => declaration.isFinal && !declaration.isLate;
void capture() {
assert(!isAllocated);
@@ -250,7 +247,7 @@ class Frame {
int frameSize = 0;
List<int> temporaries = <int>[];
int? contextLevelAtEntry;
bool capturesOnlyFinalAndSharedVars = true;
bool capturesOnlyFinalNotLateVars = true;
Frame(this.function, this.parent);
@@ -310,9 +307,7 @@ class _ScopeBuilder extends RecursiveVisitor {
List<TreeNode> _enclosingTryCatches = const [];
int _loopDepth = 0;
final PragmaAnnotationParser _pragmaParser;
_ScopeBuilder(this._pragmaParser, this.locals);
_ScopeBuilder(this.locals);
List<VariableDeclaration> _sortNamedParameters(FunctionNode function) {
final params = function.namedParameters.toList();
@@ -472,10 +467,7 @@ class _ScopeBuilder extends RecursiveVisitor {
if (scope == null) {
scope = _currentScope;
}
final isShared = _pragmaParser
.parsedPragmas<ParsedVmSharedPragma>(variable.annotations)
.isNotEmpty;
final VarDesc v = new VarDesc(variable, scope, isShared);
final VarDesc v = new VarDesc(variable, scope);
assert(locals._vars[variable] == null,
'Double declaring variable ${variable}!');
locals._vars[variable] = v;
@@ -488,8 +480,8 @@ class _ScopeBuilder extends RecursiveVisitor {
}
if (v.frame != _currentFrame) {
v.capture();
if (!v.isFinalOrShared) {
_currentFrame.capturesOnlyFinalAndSharedVars = false;
if (!v.isFinalNotLate) {
_currentFrame.capturesOnlyFinalNotLateVars = false;
}
}
}
+4 -4
View File
@@ -304,14 +304,14 @@ void BytecodeReaderHelper::ReadCode(const Function& function,
(flags & ClosureCode::kHasSourcePositionsFlag) != 0;
const bool has_local_variables =
(flags & ClosureCode::kHasLocalVariablesFlag) != 0;
const bool does_close_over_only_final_and_shared_vars =
(flags & ClosureCode::kCapturesOnlyFinalAndSharedVarsFlag) != 0;
const bool captures_only_final_not_late_vars =
(flags & ClosureCode::kCapturesOnlyFinalNotLateVarsFlag) != 0;
// Read closure bytecode and attach to closure function.
closure_bytecode = ReadBytecode(pool);
closure.set_does_close_over_only_final_and_shared_vars(
does_close_over_only_final_and_shared_vars);
closure.set_captures_only_final_not_late_vars(
captures_only_final_not_late_vars);
closure.AttachBytecode(closure_bytecode);
ReadExceptionsTable(closure, closure_bytecode, has_exceptions_table);
+1 -1
View File
@@ -308,7 +308,7 @@ class BytecodeReaderHelper : public ValueObject {
static const int kHasExceptionsTableFlag = 1 << 0;
static const int kHasSourcePositionsFlag = 1 << 1;
static const int kHasLocalVariablesFlag = 1 << 2;
static const int kCapturesOnlyFinalAndSharedVarsFlag = 1 << 3;
static const int kCapturesOnlyFinalNotLateVarsFlag = 1 << 3;
};
// Parameter flags, must be in sync with ParameterFlags constants in
+4 -5
View File
@@ -399,12 +399,11 @@ void FfiCallbackMetadata::EnsureOnlyTriviallyImmutableValuesInClosure(
ValidateTriviallyImmutabilityOfAnObject(zone, &obj, context.At(i));
}
if (!function.does_close_over_only_final_and_shared_vars()) {
if (!function.captures_only_final_not_late_vars()) {
const String& error = String::Handle(
zone,
String::New(
"Only final and 'vm:shared' variables can be captured by isolate "
"group callbacks."));
zone, String::New(
"Only final not-late variables can be captured by isolate "
"group callbacks."));
Exceptions::ThrowArgumentError(error);
UNREACHABLE();
}
+6 -9
View File
@@ -8605,22 +8605,20 @@ void Function::set_awaiter_link(Function::AwaiterLink link) const {
UNREACHABLE();
}
bool Function::does_close_over_only_final_and_shared_vars() const {
bool Function::captures_only_final_not_late_vars() const {
if (IsClosureFunction()) {
const Object& obj = Object::Handle(untag()->data());
ASSERT(!obj.IsNull());
return ClosureData::Cast(obj).does_close_over_only_final_and_shared_vars();
return ClosureData::Cast(obj).captures_only_final_not_late_vars();
}
UNREACHABLE();
}
void Function::set_does_close_over_only_final_and_shared_vars(
bool value) const {
void Function::set_captures_only_final_not_late_vars(bool value) const {
if (IsClosureFunction()) {
const Object& obj = Object::Handle(untag()->data());
ASSERT(!obj.IsNull());
ClosureData::Cast(obj).set_does_close_over_only_final_and_shared_vars(
value);
ClosureData::Cast(obj).set_captures_only_final_not_late_vars(value);
return;
}
UNREACHABLE();
@@ -12208,13 +12206,12 @@ void ClosureData::set_awaiter_link(Function::AwaiterLink link) const {
link.index);
}
bool ClosureData::does_close_over_only_final_and_shared_vars() const {
bool ClosureData::captures_only_final_not_late_vars() const {
return untag()
->packed_fields_.Read<UntaggedClosureData::CapturesOnlySharedFields>();
}
void ClosureData::set_does_close_over_only_final_and_shared_vars(
bool value) const {
void ClosureData::set_captures_only_final_not_late_vars(bool value) const {
untag()->packed_fields_.Update<UntaggedClosureData::CapturesOnlySharedFields>(
value);
}
+4 -4
View File
@@ -3321,8 +3321,8 @@ class Function : public Object {
(awaiter_link().depth != UntaggedClosureData::kNoAwaiterLinkDepth);
}
void set_does_close_over_only_final_and_shared_vars(bool value) const;
bool does_close_over_only_final_and_shared_vars() const;
void set_captures_only_final_not_late_vars(bool value) const;
bool captures_only_final_not_late_vars() const;
// Enclosing function of this local function.
FunctionPtr parent_function() const;
@@ -4417,8 +4417,8 @@ class ClosureData : public Object {
Function::AwaiterLink awaiter_link() const;
void set_awaiter_link(Function::AwaiterLink link) const;
bool does_close_over_only_final_and_shared_vars() const;
void set_does_close_over_only_final_and_shared_vars(bool value) const;
bool captures_only_final_not_late_vars() const;
void set_captures_only_final_not_late_vars(bool value) const;
// Enclosing function of this local function.
PRECOMPILER_WSR_FIELD_DECLARATION(Function, parent_function)
+6 -5
View File
@@ -462,7 +462,7 @@ ContextScopePtr LocalScope::PreserveOuterScope(
LocalVariable* awaiter_link = nullptr;
bool does_capture_only_final_and_shared_vars = true;
bool captures_only_final_not_late_vars = true;
// Create a descriptor for each referenced captured variable of enclosing
// functions to preserve its name and its context allocation information.
@@ -505,8 +505,9 @@ ContextScopePtr LocalScope::PreserveOuterScope(
bool is_shared = variable->ComputeIfShared(library);
context_scope.SetIsSharedAt(captured_idx, is_shared);
if (!is_shared && !variable->is_final()) {
does_capture_only_final_and_shared_vars = false;
// late final variables are not allowed, only final are.
if (!variable->is_final() || variable->is_late()) {
captures_only_final_not_late_vars = false;
}
captured_idx++;
@@ -533,8 +534,8 @@ ContextScopePtr LocalScope::PreserveOuterScope(
}
if (!function.IsNull()) {
function.set_does_close_over_only_final_and_shared_vars(
does_capture_only_final_and_shared_vars);
function.set_captures_only_final_not_late_vars(
captures_only_final_not_late_vars);
}
return context_scope.ptr();
@@ -33,13 +33,14 @@ Future<void> testCapturedLocalVarPragmaVmShared() async {
@pragma('vm:shared')
// ignore: unused_local_variable
int foo_result = 42;
final callback = NativeCallable<CallbackNativeType>.isolateGroupBound((
int a,
int b,
) {
foo_result += (a * b);
});
callback.close();
Expect.throws(() {
final callback = NativeCallable<CallbackNativeType>.isolateGroupBound((
int a,
int b,
) {
foo_result += (a * b);
});
}, (e) => e.toString().contains("Only final not-late variables can be"));
}
Future<void> testCapturedLocalVarFinal() async {
+271 -144
View File
@@ -22,73 +22,51 @@ import "package:expect/expect.dart";
import "run_isolate_group_run_test.dart" deferred as lib1;
var foo = 42;
var foo_no_initializer;
main(List<String> args) {
testUpdateSharedVar();
testReturnsConstant();
testReturnsList();
testReturnsNotSharedFinal();
testUpdateSharedVarWithNoInitializer();
@pragma('vm:shared')
var shared_foo_no_initializer;
testFailToAccessNotSharedVarWithInitializer();
testFailToAccessNotSharedVarWithoutInitializer();
@pragma('vm:shared')
final foo_final = 1234;
testFailToCaptureLateFinalVar();
testCapturesFinalNotSharedVar();
@pragma('vm:never-inline')
updateFoo() {
foo = 56;
}
@pragma('vm:never-inline')
updateFooNoInitializer() {
foo_no_initializer = 78;
}
class Baz {
static late final foo;
}
@pragma('vm:never-inline')
bar() {
Baz.foo = 42;
testUpdateNotSharedStaticField();
testUpdateSharedStringStaticVar();
testClosure();
testFailToPrint();
testFailToIsolateGroupRunSyncThrows();
testIsolateCurrent();
testFailToIsolateExit();
testFailToIsolateSpawn();
testStringMethodTearoff();
testListMethodTearoff(args);
testFailToReceivePort();
testFailToDeferredLibrary();
testFailToEnvironment();
testUserTag();
testDoubleToString();
testBase64Decoder();
testRandom();
testEncoding();
print("All tests completed :)");
}
///
@pragma('vm:shared')
var list_length = 0;
@pragma('vm:shared')
String string_foo = "";
@pragma('vm:shared')
SendPort? sp;
StringMethodTearoffTest() {
@pragma('vm:shared')
final stringTearoff = "abc".toString;
IsolateGroup.runSync(() {
stringTearoff;
});
}
ListMethodTearoffTest(List<String> args) {
final listTearoff = args.insert;
Expect.throws(
() {
IsolateGroup.runSync(() {
listTearoff;
});
},
(e) =>
e is ArgumentError && e.toString().contains("Only trivially-immutable"),
);
}
thefun() {}
@pragma('vm:shared')
String default_tag = "";
@pragma('vm:shared')
double pi = 3.14159;
main(List<String> args) {
void testUpdateSharedVar() {
IsolateGroup.runSync(() {
final l = <int>[];
for (int i = 0; i < 100; i++) {
@@ -97,67 +75,35 @@ main(List<String> args) {
list_length = l.length;
});
Expect.equals(100, list_length);
}
Expect.equals(42, IsolateGroup.runSync(() => 42));
Expect.listEquals([1, 2, 3], IsolateGroup.runSync(() => [1, 2, 3]));
///
@pragma('vm:shared')
final foo_final = 1234;
void testReturnsNotSharedFinal() {
Expect.equals(1234, IsolateGroup.runSync(() => foo_final));
}
IsolateGroup.runSync(() {
shared_foo_no_initializer = 2345;
});
Expect.equals(2345, IsolateGroup.runSync(() => shared_foo_no_initializer));
///
void testReturnsConstant() {
Expect.equals(42, IsolateGroup.runSync(() => 42));
}
Expect.throws(
() {
IsolateGroup.runSync(() {
throw "error";
});
},
(e) => e == "error",
'Expect thrown error',
);
///
void testReturnsList() {
Expect.listEquals([1, 2, 3], IsolateGroup.runSync(() => [1, 2, 3]));
}
// Documenting current limitations.
Expect.notEquals(() {
IsolateGroup.runSync(() {
return Isolate.current;
});
}, Isolate.current);
///
var foo_no_initializer;
Expect.throws(
() {
IsolateGroup.runSync(() {
print('42');
});
},
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error printing',
);
updateFoo();
Expect.throws(
() {
IsolateGroup.runSync(() {
return foo;
});
},
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error accessing',
);
Expect.throws(
() {
IsolateGroup.runSync(() {
foo = 123;
});
},
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error accessing',
);
Expect.equals(56, foo);
@pragma('vm:never-inline')
updateFooNoInitializer() {
foo_no_initializer = 78;
}
void testFailToAccessNotSharedVarWithoutInitializer() {
updateFooNoInitializer();
Expect.throws(
() {
@@ -179,34 +125,156 @@ main(List<String> args) {
'Expect error accessing',
);
Expect.equals(78, foo_no_initializer);
}
{
bar();
Expect.equals(42, Baz.foo);
}
///
void testFailToCaptureLateFinalVar() {
late final int late_final_var;
late_final_var = 12;
Expect.throws(() {
IsolateGroup.runSync(() {
return late_final_var;
});
}, (e) => e is Error && e.toString().contains("Only final"));
}
IsolateGroup.runSync(() {
string_foo = "foo bar";
});
Expect.equals("foo bar", string_foo);
///
@pragma('vm:never-inline')
calculateTwelve() => 12;
void testCapturesFinalNotSharedVar() {
final int late_final_var = calculateTwelve();
Expect.equals(late_final_var, IsolateGroup.runSync(() => late_final_var));
}
///
var foo = 42;
@pragma('vm:never-inline')
updateFoo() {
foo = 56;
}
void testFailToAccessNotSharedVarWithInitializer() {
updateFoo();
Expect.throws(
() {
IsolateGroup.runSync(() {
return foo;
});
},
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error accessing',
);
Expect.throws(
() {
IsolateGroup.runSync(() {
ReceivePort();
foo = 123;
});
},
(e) =>
e is ArgumentError &&
e.toString().contains("Only available when running in context"),
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error accessing',
);
Expect.equals(56, foo);
}
///
@pragma('vm:shared')
var shared_foo_no_initializer;
void testUpdateSharedVarWithNoInitializer() {
IsolateGroup.runSync(() {
shared_foo_no_initializer = 2345;
});
Expect.equals(2345, IsolateGroup.runSync(() => shared_foo_no_initializer));
}
///
class Baz {
static late final foo;
}
@pragma('vm:never-inline')
updateBazFoo() {
Baz.foo = 42;
}
void testUpdateNotSharedStaticField() {
updateBazFoo();
Expect.equals(42, Baz.foo);
}
///
@pragma('vm:shared')
String string_foo = "";
void testUpdateSharedStringStaticVar() {
IsolateGroup.runSync(() {
string_foo = "foo bar";
});
Expect.equals("foo bar", string_foo);
}
///
@pragma('vm:never-inline')
@pragma('vm:shared')
var closure = () {
return 42;
};
void testClosure() {
final result = IsolateGroup.runSync(() {
return closure();
});
Expect.equals(42, result);
}
///
void testFailToPrint() {
Expect.throws(
() {
IsolateGroup.runSync(() {
print('42');
});
},
(e) => e is Error && e.toString().contains("AccessError"),
'Expect error printing',
);
}
///
void testFailToIsolateGroupRunSyncThrows() {
Expect.throws(
() {
IsolateGroup.runSync(() {
throw "error";
});
},
(e) => e == "error",
'Expect thrown error',
);
}
///
void testIsolateCurrent() {
Expect.notEquals(() {
IsolateGroup.runSync(() {
return Isolate.current;
});
}, Isolate.current);
}
///
void testFailToIsolateExit() {
Expect.throws(() {
IsolateGroup.runSync(() {
Isolate.exit();
});
}, (e) => e.toString().contains("Attempt to access isolate static field"));
}
///
void testFailToIsolateSpawn() {
Expect.throws(() {
IsolateGroup.runSync(() {
Isolate.spawn((_) {}, null);
@@ -218,44 +286,96 @@ main(List<String> args) {
Isolate.spawnUri(Uri.parse("http://127.0.0.1"), [], (_) {});
});
}, (e) => e.toString().contains("Attempt to access isolate static field"));
}
StringMethodTearoffTest();
ListMethodTearoffTest(args);
///
testStringMethodTearoff() {
@pragma('vm:shared')
final stringTearoff = "abc".toString;
IsolateGroup.runSync(() {
stringTearoff;
});
}
{
final rp = ReceivePort();
Expect.throws(
() {
IsolateGroup.runSync(() {
sp = rp.sendPort;
});
},
(e) =>
e is ArgumentError &&
e.toString().contains("Only trivially-immutable"),
);
rp.close();
}
///
testListMethodTearoff(List<String> args) {
final listTearoff = args.insert;
Expect.throws(
() {
IsolateGroup.runSync(() {
listTearoff;
});
},
(e) =>
e is ArgumentError && e.toString().contains("Only trivially-immutable"),
);
}
// deferred libraries can't be used from isolate group callbacks.
///
@pragma('vm:shared')
SendPort? sp;
void testFailToReceivePort() {
Expect.throws(
() {
IsolateGroup.runSync(() {
ReceivePort();
});
},
(e) =>
e is ArgumentError &&
e.toString().contains("Only available when running in context"),
);
final rp = ReceivePort();
Expect.throws(
() {
IsolateGroup.runSync(() {
sp = rp.sendPort;
});
},
(e) =>
e is ArgumentError && e.toString().contains("Only trivially-immutable"),
);
rp.close();
}
///
thefun() {}
void testFailToDeferredLibrary() {
Expect.throws(() {
IsolateGroup.runSync(() {
lib1.thefun();
});
}, (e) => e is ArgumentError && e.toString().contains("Only available when"));
}
// environment can't be accessed from isolate group callbacks.
///
void testFailToEnvironment() {
Expect.throws(() {
IsolateGroup.runSync(() {
new bool.hasEnvironment("Anything");
});
}, (e) => e is ArgumentError && e.toString().contains("Only available when"));
}
///
@pragma('vm:shared')
String default_tag = "";
void testUserTag() {
IsolateGroup.runSync(() {
default_tag = UserTag.defaultTag.toString();
});
Expect.notEquals("", default_tag);
}
///
@pragma('vm:shared')
double pi = 3.14159;
void testDoubleToString() {
final result = IsolateGroup.runSync(() {
return pi.toString();
});
@@ -264,18 +384,27 @@ main(List<String> args) {
return identical(pi.toString(), pi.toString());
});
Expect.isTrue(resultIdentical);
}
///
void testBase64Decoder() {
Expect.listEquals(
"abcdefghijklmnopqrstuvwxyz".codeUnits,
IsolateGroup.runSync(() {
return Base64Decoder().convert("YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=");
}),
);
}
///
void testRandom() {
IsolateGroup.runSync(() {
Random().nextInt(10);
});
}
///
void testEncoding() {
Expect.listEquals(
[0x31, 0x32, 0x33, 0x61, 0x62, 0x63],
IsolateGroup.runSync(
@@ -290,6 +419,4 @@ main(List<String> args) {
utf8,
IsolateGroup.runSync(() => Encoding.getByName("utf-8")),
);
print("All tests completed :)");
}