// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:convert'; import 'dart:typed_data'; import 'package:package_config/package_config_types.dart'; import 'package:package_config/src/errors.dart'; import 'package:package_config/src/package_config_json.dart'; import 'package:package_config/src/packages_file.dart' as packages; import 'package:test/test.dart'; import 'src/util.dart'; void main() { group('.packages', () { test('valid', () { var packagesFile = '# Generated by pub yadda yadda\n' 'foo:file:///foo/lib/\n' 'bar:/bar/lib/\n' 'baz:lib/\n'; var result = packages.parse(utf8.encode(packagesFile), Uri.parse('file:///tmp/file.dart'), throwError); expect(result.version, 1); expect({for (var p in result.packages) p.name}, {'foo', 'bar', 'baz'}); expect(result.resolve(pkg('foo', 'foo.dart')), Uri.parse('file:///foo/lib/foo.dart')); expect(result.resolve(pkg('bar', 'bar.dart')), Uri.parse('file:///bar/lib/bar.dart')); expect(result.resolve(pkg('baz', 'baz.dart')), Uri.parse('file:///tmp/lib/baz.dart')); var foo = result['foo']!; expect(foo, isNotNull); expect(foo.root, Uri.parse('file:///foo/')); expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/')); expect(foo.languageVersion, LanguageVersion(2, 7)); expect(foo.relativeRoot, false); }); test('valid empty', () { var packagesFile = '# Generated by pub yadda yadda\n'; var result = packages.parse( utf8.encode(packagesFile), Uri.file('/tmp/file.dart'), throwError); expect(result.version, 1); expect({for (var p in result.packages) p.name}, {}); }); group('invalid', () { var baseFile = Uri.file('/tmp/file.dart'); void testThrows(String name, String content) { test(name, () { expect( () => packages.parse(utf8.encode(content), baseFile, throwError), throwsA(isA())); }); test('$name, handle error', () { var hadError = false; packages.parse(utf8.encode(content), baseFile, (error) { hadError = true; expect(error, isA()); }); expect(hadError, true); }); } testThrows('repeated package name', 'foo:lib/\nfoo:lib\n'); testThrows('no colon', 'foo\n'); testThrows('empty package name', ':lib/\n'); testThrows('dot only package name', '.:lib/\n'); testThrows('dot only package name', '..:lib/\n'); testThrows('invalid package name character', 'f\\o:lib/\n'); testThrows('package URI', 'foo:package:bar/lib/'); testThrows('location with query', 'f\\o:lib/?\n'); testThrows('location with fragment', 'f\\o:lib/#\n'); }); }); group('package_config.json', () { test('valid', () { var packageConfigFile = ''' { "configVersion": 2, "packages": [ { "name": "foo", "rootUri": "file:///foo/", "packageUri": "lib/", "languageVersion": "2.5", "nonstandard": true }, { "name": "bar", "rootUri": "/bar/", "packageUri": "lib/", "languageVersion": "9999.9999" }, { "name": "baz", "rootUri": "../", "packageUri": "lib/" }, { "name": "noslash", "rootUri": "../noslash", "packageUri": "lib" } ], "generator": "pub", "other": [42] } '''; var config = parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode(packageConfigFile) as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect({for (var p in config.packages) p.name}, {'foo', 'bar', 'baz', 'noslash'}); expect(config.resolve(pkg('foo', 'foo.dart')), Uri.parse('file:///foo/lib/foo.dart')); expect(config.resolve(pkg('bar', 'bar.dart')), Uri.parse('file:///bar/lib/bar.dart')); expect(config.resolve(pkg('baz', 'baz.dart')), Uri.parse('file:///tmp/lib/baz.dart')); var foo = config['foo']!; expect(foo, isNotNull); expect(foo.root, Uri.parse('file:///foo/')); expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/')); expect(foo.languageVersion, LanguageVersion(2, 5)); expect(foo.extraData, {'nonstandard': true}); expect(foo.relativeRoot, false); var bar = config['bar']!; expect(bar, isNotNull); expect(bar.root, Uri.parse('file:///bar/')); expect(bar.packageUriRoot, Uri.parse('file:///bar/lib/')); expect(bar.languageVersion, LanguageVersion(9999, 9999)); expect(bar.extraData, null); expect(bar.relativeRoot, false); var baz = config['baz']!; expect(baz, isNotNull); expect(baz.root, Uri.parse('file:///tmp/')); expect(baz.packageUriRoot, Uri.parse('file:///tmp/lib/')); expect(baz.languageVersion, null); expect(baz.relativeRoot, true); // No slash after root or package root. One is inserted. var noslash = config['noslash']!; expect(noslash, isNotNull); expect(noslash.root, Uri.parse('file:///tmp/noslash/')); expect(noslash.packageUriRoot, Uri.parse('file:///tmp/noslash/lib/')); expect(noslash.languageVersion, null); expect(noslash.relativeRoot, true); expect(config.extraData, { 'generator': 'pub', 'other': [42] }); }); test('valid other order', () { // The ordering in the file is not important. var packageConfigFile = ''' { "generator": "pub", "other": [42], "packages": [ { "languageVersion": "2.5", "packageUri": "lib/", "rootUri": "file:///foo/", "name": "foo" }, { "packageUri": "lib/", "languageVersion": "9999.9999", "rootUri": "/bar/", "name": "bar" }, { "packageUri": "lib/", "name": "baz", "rootUri": "../" } ], "configVersion": 2 } '''; var config = parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode(packageConfigFile) as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect({for (var p in config.packages) p.name}, {'foo', 'bar', 'baz'}); expect(config.resolve(pkg('foo', 'foo.dart')), Uri.parse('file:///foo/lib/foo.dart')); expect(config.resolve(pkg('bar', 'bar.dart')), Uri.parse('file:///bar/lib/bar.dart')); expect(config.resolve(pkg('baz', 'baz.dart')), Uri.parse('file:///tmp/lib/baz.dart')); expect(config.extraData, { 'generator': 'pub', 'other': [42] }); }); // Check that a few minimal configurations are valid. // These form the basis of invalid tests below. var cfg = '"configVersion":2'; var pkgs = '"packages":[]'; var name = '"name":"foo"'; var root = '"rootUri":"/foo/"'; test('minimal', () { var config = parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode('{$cfg,$pkgs}') as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect(config.packages, isEmpty); }); test('minimal package', () { // A package must have a name and a rootUri, the remaining properties // are optional. var config = parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode('{$cfg,"packages":[{$name,$root}]}') as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect(config.packages.first.name, 'foo'); }); test('nested packages', () { var configBytes = utf8.encode(json.encode({ 'configVersion': 2, 'packages': [ {'name': 'foo', 'rootUri': '/foo/', 'packageUri': 'lib/'}, {'name': 'bar', 'rootUri': '/foo/bar/', 'packageUri': 'lib/'}, {'name': 'baz', 'rootUri': '/foo/bar/baz/', 'packageUri': 'lib/'}, {'name': 'qux', 'rootUri': '/foo/qux/', 'packageUri': 'lib/'}, ] })); // ignore: unnecessary_cast var config = parsePackageConfigBytes(configBytes as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect(config.packageOf(Uri.parse('file:///foo/lala/lala.dart'))!.name, 'foo'); expect(config.packageOf(Uri.parse('file:///foo/bar/lala.dart'))!.name, 'bar'); expect(config.packageOf(Uri.parse('file:///foo/bar/baz/lala.dart'))!.name, 'baz'); expect(config.packageOf(Uri.parse('file:///foo/qux/lala.dart'))!.name, 'qux'); expect(config.toPackageUri(Uri.parse('file:///foo/lib/diz')), Uri.parse('package:foo/diz')); expect(config.toPackageUri(Uri.parse('file:///foo/bar/lib/diz')), Uri.parse('package:bar/diz')); expect(config.toPackageUri(Uri.parse('file:///foo/bar/baz/lib/diz')), Uri.parse('package:baz/diz')); expect(config.toPackageUri(Uri.parse('file:///foo/qux/lib/diz')), Uri.parse('package:qux/diz')); }); test('nested packages 2', () { var configBytes = utf8.encode(json.encode({ 'configVersion': 2, 'packages': [ {'name': 'foo', 'rootUri': '/', 'packageUri': 'lib/'}, {'name': 'bar', 'rootUri': '/bar/', 'packageUri': 'lib/'}, {'name': 'baz', 'rootUri': '/bar/baz/', 'packageUri': 'lib/'}, {'name': 'qux', 'rootUri': '/qux/', 'packageUri': 'lib/'}, ] })); // ignore: unnecessary_cast var config = parsePackageConfigBytes(configBytes as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect( config.packageOf(Uri.parse('file:///lala/lala.dart'))!.name, 'foo'); expect(config.packageOf(Uri.parse('file:///bar/lala.dart'))!.name, 'bar'); expect(config.packageOf(Uri.parse('file:///bar/baz/lala.dart'))!.name, 'baz'); expect(config.packageOf(Uri.parse('file:///qux/lala.dart'))!.name, 'qux'); expect(config.toPackageUri(Uri.parse('file:///lib/diz')), Uri.parse('package:foo/diz')); expect(config.toPackageUri(Uri.parse('file:///bar/lib/diz')), Uri.parse('package:bar/diz')); expect(config.toPackageUri(Uri.parse('file:///bar/baz/lib/diz')), Uri.parse('package:baz/diz')); expect(config.toPackageUri(Uri.parse('file:///qux/lib/diz')), Uri.parse('package:qux/diz')); }); test('packageOf is case sensitive on windows', () { var configBytes = utf8.encode(json.encode({ 'configVersion': 2, 'packages': [ {'name': 'foo', 'rootUri': 'file:///C:/Foo/', 'packageUri': 'lib/'}, ] })); var config = parsePackageConfigBytes( // ignore: unnecessary_cast configBytes as Uint8List, Uri.parse('file:///C:/tmp/.dart_tool/file.dart'), throwError); expect(config.version, 2); expect( config.packageOf(Uri.parse('file:///C:/foo/lala/lala.dart')), null); expect(config.packageOf(Uri.parse('file:///C:/Foo/lala/lala.dart'))!.name, 'foo'); }); group('invalid', () { void testThrows(String name, String source) { test(name, () { expect( // ignore: unnecessary_cast () => parsePackageConfigBytes(utf8.encode(source) as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError), throwsA(isA())); }); } void testThrowsContains( String name, String source, String containsString) { test(name, () { dynamic exception; try { parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode(source) as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError, ); } catch (e) { exception = e; } if (exception == null) fail("Didn't get exception"); expect('$exception', contains(containsString)); }); } testThrows('comment', '# comment\n {$cfg,$pkgs}'); testThrows('.packages file', 'foo:/foo\n'); testThrows('no configVersion', '{$pkgs}'); testThrows('no packages', '{$cfg}'); group('config version:', () { testThrows('null', '{"configVersion":null,$pkgs}'); testThrows('string', '{"configVersion":"2",$pkgs}'); testThrows('array', '{"configVersion":[2],$pkgs}'); }); group('packages:', () { testThrows('null', '{$cfg,"packages":null}'); testThrows('string', '{$cfg,"packages":"foo"}'); testThrows('object', '{$cfg,"packages":{}}'); }); group('packages entry:', () { testThrows('null', '{$cfg,"packages":[null]}'); testThrows('string', '{$cfg,"packages":["foo"]}'); testThrows('array', '{$cfg,"packages":[[]]}'); }); group('package', () { testThrows('no name', '{$cfg,"packages":[{$root}]}'); group('name:', () { testThrows('null', '{$cfg,"packages":[{"name":null,$root}]}'); testThrows('num', '{$cfg,"packages":[{"name":1,$root}]}'); testThrows('object', '{$cfg,"packages":[{"name":{},$root}]}'); testThrows('empty', '{$cfg,"packages":[{"name":"",$root}]}'); testThrows('one-dot', '{$cfg,"packages":[{"name":".",$root}]}'); testThrows('two-dot', '{$cfg,"packages":[{"name":"..",$root}]}'); testThrows( "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}'); testThrows( "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}'); testThrows( "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}'); }); testThrows('no root', '{$cfg,"packages":[{$name}]}'); group('root:', () { testThrows('null', '{$cfg,"packages":[{$name,"rootUri":null}]}'); testThrows('num', '{$cfg,"packages":[{$name,"rootUri":1}]}'); testThrows('object', '{$cfg,"packages":[{$name,"rootUri":{}}]}'); testThrows('fragment', '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}'); testThrows('query', '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}'); testThrows('package-URI', '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}'); }); group('package-URI root:', () { testThrows( 'null', '{$cfg,"packages":[{$name,$root,"packageUri":null}]}'); testThrows('num', '{$cfg,"packages":[{$name,$root,"packageUri":1}]}'); testThrows( 'object', '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}'); testThrows('fragment', '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}'); testThrows( 'query', '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}'); testThrows('package: URI', '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}'); testThrows('not inside root', '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}'); }); group('language version', () { testThrows('null', '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}'); testThrows( 'num', '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}'); testThrows('object', '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}'); testThrows('empty', '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}'); testThrows('non number.number', '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}'); testThrows('number.non number', '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}'); testThrows('non number', '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}'); testThrows('one number', '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}'); testThrows('three numbers', '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}'); testThrows('leading zero first', '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}'); testThrows('leading zero second', '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}'); testThrows('trailing-', '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}'); testThrows('trailing+', '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}'); }); }); testThrows('duplicate package name', '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}'); testThrowsContains( // The roots of foo and bar are the same. 'same roots', '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}', 'the same root directory'); testThrowsContains( // The roots of foo and bar are the same. 'same roots 2', '{$cfg,"packages":[{$name,"rootUri":"/"},{"name":"bar","rootUri":"/"}]}', 'the same root directory'); testThrowsContains( // The root of bar is inside the root of foo, // but the package root of foo is inside the root of bar. 'between root and lib', '{$cfg,"packages":[' '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},' '{"name":"bar","rootUri":"/foo/bar/","packageUri":"baz/lib"}]}', 'package root of foo is inside the root of bar'); // This shouldn't be allowed, but for internal reasons it is. test('package inside package root', () { var config = parsePackageConfigBytes( // ignore: unnecessary_cast utf8.encode( '{$cfg,"packages":[' '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},' '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}', ) as Uint8List, Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError); expect( config .packageOf(Uri.parse('file:///foo/lib/bar/lib/lala.dart'))! .name, 'foo'); // why not bar? expect(config.toPackageUri(Uri.parse('file:///foo/lib/bar/lib/diz')), Uri.parse('package:foo/bar/lib/diz')); // why not package:bar/diz? }); }); }); group('factories', () { void testConfig(String name, PackageConfig config, PackageConfig expected) { group(name, () { test('structure', () { expect(config.version, expected.version); var expectedPackages = {for (var p in expected.packages) p.name}; var actualPackages = {for (var p in config.packages) p.name}; expect(actualPackages, expectedPackages); }); for (var package in config.packages) { var name = package.name; test('package $name', () { var expectedPackage = expected[name]!; expect(expectedPackage, isNotNull); expect(package.root, expectedPackage.root, reason: 'root'); expect(package.packageUriRoot, expectedPackage.packageUriRoot, reason: 'package root'); expect(package.languageVersion, expectedPackage.languageVersion, reason: 'languageVersion'); }); } }); } var configText = ''' {"configVersion": 2, "packages": [ { "name": "foo", "rootUri": "foo/", "packageUri": "bar/", "languageVersion": "1.2" } ]} '''; var baseUri = Uri.parse('file:///start/'); var config = PackageConfig([ Package('foo', Uri.parse('file:///start/foo/'), packageUriRoot: Uri.parse('file:///start/foo/bar/'), languageVersion: LanguageVersion(1, 2)) ]); testConfig( 'string', PackageConfig.parseString(configText, baseUri), config); testConfig( 'bytes', PackageConfig.parseBytes( Uint8List.fromList(configText.codeUnits), baseUri), config); testConfig('json', PackageConfig.parseJson(jsonDecode(configText), baseUri), config); baseUri = Uri.parse('file:///start2/'); config = PackageConfig([ Package('foo', Uri.parse('file:///start2/foo/'), packageUriRoot: Uri.parse('file:///start2/foo/bar/'), languageVersion: LanguageVersion(1, 2)) ]); testConfig( 'string2', PackageConfig.parseString(configText, baseUri), config); testConfig( 'bytes2', PackageConfig.parseBytes( Uint8List.fromList(configText.codeUnits), baseUri), config); testConfig('json2', PackageConfig.parseJson(jsonDecode(configText), baseUri), config); }); }