// 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:test/test.dart'; import 'src/util.dart'; void main() { 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( utf8.encode(packageConfigFile), 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( utf8.encode(packageConfigFile), 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( utf8.encode('{$cfg,$pkgs}'), 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( utf8.encode('{$cfg,"packages":[{$name,$root}]}'), 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/'}, ], }), ); var config = parsePackageConfigBytes( configBytes, 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/'}, ], }), ); var config = parsePackageConfigBytes( configBytes, 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( configBytes, 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( () => parsePackageConfigBytes( utf8.encode(source), Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError, ), throwsA(isA()), ); }); } void testThrowsContains( String name, String source, String containsString, ) { test(name, () { Object? exception; try { parsePackageConfigBytes( utf8.encode(source), 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( utf8.encode( '{$cfg,"packages":[' '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},' '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}', ), 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, ); }); }