| #!/usr/bin/env vpython3 | |
| # Copyright 2026 The Chromium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Unit tests for the node_modules archive tooling in manage_node_deps.py.""" | |
| import contextlib | |
| import io | |
| import os | |
| import shutil | |
| import tarfile | |
| import tempfile | |
| import textwrap | |
| import unittest | |
| from unittest import mock | |
| import manage_node_deps | |
| def write(root, relpath, content='', mode=None): | |
| full = os.path.join(root, relpath) | |
| os.makedirs(os.path.dirname(full), exist_ok=True) | |
| with open(full, 'w') as f: | |
| f.write(content) | |
| if mode is not None: | |
| os.chmod(full, mode) | |
| return full | |
| def symlink(root, relpath, target): | |
| full = os.path.join(root, relpath) | |
| os.makedirs(os.path.dirname(full), exist_ok=True) | |
| os.symlink(target, full) | |
| return full | |
| class PostProcessTest(unittest.TestCase): | |
| """The package.json rewrites that run after `npm install`.""" | |
| def setUp(self): | |
| tmpdir = tempfile.TemporaryDirectory() | |
| self.addCleanup(tmpdir.cleanup) | |
| self.tmp = tmpdir.name | |
| self.package_json = os.path.join(self.tmp, 'package.json') | |
| patcher = mock.patch.object(manage_node_deps.devtools_paths, | |
| 'package_json_path', | |
| return_value=self.package_json) | |
| patcher.start() | |
| self.addCleanup(patcher.stop) | |
| def read(self, relpath): | |
| with open(os.path.join(self.tmp, relpath), encoding='utf-8') as f: | |
| return f.read() | |
| def test_remove_package_json_entries_drops_dependencies(self): | |
| write( | |
| self.tmp, 'package.json', | |
| textwrap.dedent("""\ | |
| { | |
| "name": "devtools-frontend", | |
| "dependencies": { | |
| "left-pad": "1.0.0" | |
| }, | |
| "devDependencies": { | |
| "typescript": "5.0.0" | |
| } | |
| } | |
| """)) | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.remove_package_json_entries() | |
| self.assertFalse(failed) | |
| self.assertEqual( | |
| self.read('package.json'), | |
| textwrap.dedent("""\ | |
| { | |
| "name": "devtools-frontend", | |
| "devDependencies": { | |
| "typescript": "5.0.0" | |
| } | |
| } | |
| """)) | |
| class ArchiveTest(unittest.TestCase): | |
| def setUp(self): | |
| tmpdir = tempfile.TemporaryDirectory() | |
| self.addCleanup(tmpdir.cleanup) | |
| self.tmp = tmpdir.name | |
| self.tree = os.path.join(self.tmp, 'node_modules') | |
| os.makedirs(self.tree) | |
| def make_tree(self): | |
| write(self.tree, 'pkg/package.json', '{"name": "pkg"}') | |
| write(self.tree, 'pkg/bin/pkg.js', '#!/usr/bin/env node\n', 0o755) | |
| write(self.tree, 'pkg/index.d.ts', 'export {};') | |
| write(self.tree, '@scope/other/index.js', 'module.exports = 1;') | |
| symlink(self.tree, '.bin/pkg', '../pkg/bin/pkg.js') | |
| def archive(self, name='out.tar.gz'): | |
| out = os.path.join(self.tmp, name) | |
| digest, tar_digest = manage_node_deps.create_archive(self.tree, out) | |
| # The tar-stream digest is what --check falls back to; it must describe | |
| # exactly the bytes inside the gzip container. | |
| with open(out, 'rb') as f: | |
| self.assertEqual(manage_node_deps.tar_stream_sha256(f), tar_digest) | |
| return out, digest | |
| def test_archive_is_deterministic(self): | |
| self.make_tree() | |
| _, first = self.archive('first.tar.gz') | |
| # Metadata that must not influence the result. | |
| for dirpath, _, filenames in os.walk(self.tree): | |
| for filename in filenames: | |
| full = os.path.join(dirpath, filename) | |
| if not os.path.islink(full): | |
| os.utime(full, (1234567890, 1234567890)) | |
| os.chmod(os.path.join(self.tree, 'pkg', 'index.d.ts'), 0o600) | |
| _, second = self.archive('second.tar.gz') | |
| self.assertEqual(first, second) | |
| def test_archive_layout_and_normalization(self): | |
| self.make_tree() | |
| out, _ = self.archive() | |
| with tarfile.open(out, 'r:gz') as tar: | |
| members = tar.getmembers() | |
| names = [m.name for m in members] | |
| self.assertEqual(names, sorted(names)) | |
| self.assertIn('pkg/package.json', names) | |
| self.assertIn('@scope/other/index.js', names) | |
| self.assertNotIn('node_modules/pkg/package.json', names) | |
| by_name = {m.name: m for m in members} | |
| self.assertTrue(by_name['.bin/pkg'].issym()) | |
| self.assertEqual(by_name['.bin/pkg'].linkname, '../pkg/bin/pkg.js') | |
| self.assertEqual(by_name['pkg/bin/pkg.js'].mode, 0o755) | |
| self.assertEqual(by_name['pkg/index.d.ts'].mode, 0o644) | |
| self.assertEqual(by_name['pkg'].mode, 0o755) | |
| for member in members: | |
| self.assertEqual((member.uid, member.gid, member.uname, | |
| member.gname, member.mtime), (0, 0, '', '', 0)) | |
| def test_archive_skips_gclient_artifacts_at_root(self): | |
| self.make_tree() | |
| write(self.tree, 'node_modules.tar.gz', 'stale') | |
| write(self.tree, '.devtools-frontend_abc_hash', 'abc') | |
| write(self.tree, '.devtools-frontend_abc_is_first_class_gcs', '1') | |
| write(self.tree, '.devtools-frontend_abc_content_names', '[]') | |
| write(self.tree, 'pkg/.deep_hash', 'kept, only root artifacts skip') | |
| out, _ = self.archive() | |
| with tarfile.open(out, 'r:gz') as tar: | |
| names = tar.getnames() | |
| self.assertNotIn('node_modules.tar.gz', names) | |
| self.assertFalse([n for n in names if n.startswith('.devtools')]) | |
| self.assertIn('pkg/.deep_hash', names) | |
| def test_archive_rebuilt_from_extracted_tree_matches(self): | |
| self.make_tree() | |
| out, original = self.archive() | |
| extracted = os.path.join(self.tmp, 'extracted') | |
| os.makedirs(extracted) | |
| with tarfile.open(out, 'r:gz') as tar: | |
| tar.extractall(extracted, filter='data') | |
| # What gclient leaves behind in the dependency directory. | |
| shutil.copy(out, os.path.join(extracted, 'node_modules.tar.gz')) | |
| write(extracted, '.devtools-frontend_x_hash', original) | |
| write(extracted, '.devtools-frontend_x_is_first_class_gcs', '1') | |
| write(extracted, '.devtools-frontend_x_content_names', '[]') | |
| rebuilt, _ = manage_node_deps.create_archive( | |
| extracted, os.path.join(self.tmp, 'rebuilt.tar.gz')) | |
| self.assertEqual(original, rebuilt) | |
| def test_archive_rejects_dangling_symlink(self): | |
| self.make_tree() | |
| symlink(self.tree, '.bin/gone', '../gone/bin/gone.js') | |
| with self.assertRaises(ValueError): | |
| self.archive() | |
| def test_archive_rejects_symlink_escaping_tree(self): | |
| self.make_tree() | |
| outside = write(self.tmp, 'outside.js', '') | |
| symlink(self.tree, '.bin/outside', outside) | |
| with self.assertRaises(ValueError): | |
| self.archive() | |
| def test_archive_rejects_missing_or_empty_tree(self): | |
| out = os.path.join(self.tmp, 'out.tar.gz') | |
| with self.assertRaises(ValueError): | |
| manage_node_deps.create_archive( | |
| os.path.join(self.tmp, 'does_not_exist'), out) | |
| with self.assertRaises(ValueError): | |
| manage_node_deps.create_archive(self.tree, out) # exists, empty | |
| # Only gclient artifacts present is still "empty". | |
| write(self.tree, 'node_modules.tar.gz', 'stale') | |
| with self.assertRaises(ValueError): | |
| manage_node_deps.create_archive(self.tree, out) | |
| self.assertFalse(os.path.exists(out)) | |
| class DepsTest(unittest.TestCase): | |
| DEPS = textwrap.dedent("""\ | |
| deps = { | |
| 'third_party/esbuild': { | |
| 'packages': [{'package': 'infra/3pp/tools/esbuild/${{platform}}', | |
| 'version': 'version:3@0.28.2.chromium.2'}], | |
| 'dep_type': 'cipd', | |
| }, | |
| 'node_modules': { | |
| 'dep_type': 'gcs', | |
| 'bucket': 'chromium-nodejs', | |
| 'condition': 'non_git_source', | |
| 'objects': [ | |
| { | |
| 'object_name': 'devtools-frontend/%s', | |
| 'sha256sum': '%s', | |
| 'size_bytes': 33012345, | |
| 'generation': 1790681952304657, | |
| 'output_file': 'node_modules.tar.gz', | |
| }, | |
| ], | |
| }, | |
| 'third_party/node/linux': { | |
| 'dep_type': 'gcs', | |
| 'bucket': 'chromium-nodejs', | |
| 'objects': [{'object_name': 'other', 'sha256sum': '%s', | |
| 'size_bytes': 1, 'generation': 2}], | |
| }, | |
| } | |
| """) % ('a' * 64, 'a' * 64, 'b' * 64) | |
| def test_parse_deps_entry(self): | |
| entry = manage_node_deps.parse_deps_entry(self.DEPS) | |
| self.assertEqual( | |
| entry, { | |
| 'object_name': 'devtools-frontend/' + 'a' * 64, | |
| 'sha256sum': 'a' * 64, | |
| 'size_bytes': 33012345, | |
| 'generation': 1790681952304657, | |
| }) | |
| def test_parse_deps_entry_missing(self): | |
| self.assertIsNone( | |
| manage_node_deps.parse_deps_entry(self.DEPS, 'does_not_exist')) | |
| self.assertIsNone( | |
| manage_node_deps.parse_deps_entry(self.DEPS, | |
| 'third_party/esbuild')) | |
| def test_parse_deps_entry_accepts_double_quotes(self): | |
| # DEPS mixes quote styles (see the ai_evals entry in DEPS). | |
| entry = manage_node_deps.parse_deps_entry(self.DEPS.replace("'", '"')) | |
| self.assertEqual(entry['object_name'], 'devtools-frontend/' + 'a' * 64) | |
| self.assertEqual(entry['size_bytes'], 33012345) | |
| def test_format_deps_entry_round_trips(self): | |
| snippet = manage_node_deps.format_deps_entry( | |
| 'devtools-frontend/' + 'e' * 64, 'e' * 64, 42, 7) | |
| deps_text = 'deps = {\n%s\n}\n' % snippet | |
| self.assertEqual( | |
| manage_node_deps.parse_deps_entry(deps_text), { | |
| 'object_name': 'devtools-frontend/' + 'e' * 64, | |
| 'sha256sum': 'e' * 64, | |
| 'size_bytes': 42, | |
| 'generation': 7, | |
| }) | |
| self.assertIn("'output_file': 'node_modules.tar.gz'", snippet) | |
| self.assertIn("'condition': 'non_git_source'", snippet) | |
| def test_extract_json_blob(self): | |
| output = ('Uploading out/node_modules.tar.gz as gs://x/y\n' | |
| '{\n "path": {"objects": [{"generation": 5}]}\n}\n') | |
| blob = manage_node_deps.extract_json_blob(output) | |
| self.assertEqual(blob['path']['objects'][0]['generation'], 5) | |
| with self.assertRaises(ValueError): | |
| manage_node_deps.extract_json_blob('no json here') | |
| def test_update_deps_without_entry_prints_snippet(self): | |
| with tempfile.TemporaryDirectory() as tmp: | |
| deps = write(tmp, 'DEPS', "deps = {\n 'a': {'url': 'x'},\n}\n") | |
| out = io.StringIO() | |
| with contextlib.redirect_stdout(out): | |
| failed = manage_node_deps.update_deps( | |
| deps, 'devtools-frontend/' + 'c' * 64, 'c' * 64, 10, 7) | |
| self.assertFalse(failed) | |
| # Whatever was printed must be a valid entry once pasted into DEPS. | |
| printed = out.getvalue()[out.getvalue().index(" 'node_modules'"):] | |
| self.assertEqual( | |
| manage_node_deps.parse_deps_entry('deps = {\n%s}\n' % | |
| printed)['sha256sum'], 'c' * 64) | |
| def test_update_deps_runs_setdep(self): | |
| with tempfile.TemporaryDirectory() as tmp: | |
| deps = write(tmp, 'DEPS', self.DEPS) | |
| with mock.patch.object(manage_node_deps.subprocess, | |
| 'check_call') as check_call: | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.update_deps( | |
| deps, 'devtools-frontend/' + 'd' * 64, 'd' * 64, 11, 9) | |
| self.assertFalse(failed) | |
| cmd = check_call.call_args[0][0] | |
| self.assertIn('setdep', cmd) | |
| self.assertIn( | |
| 'node_modules@devtools-frontend/%s,%s,11,9' % ('d' * 64, 'd' * 64), | |
| cmd) | |
| self.assertIn('--deps-file=%s' % deps, cmd) | |
| def test_update_deps_is_noop_when_current(self): | |
| with tempfile.TemporaryDirectory() as tmp: | |
| deps = write(tmp, 'DEPS', self.DEPS) | |
| with mock.patch.object(manage_node_deps.subprocess, | |
| 'check_call') as check_call: | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.update_deps( | |
| deps, 'devtools-frontend/' + 'a' * 64, 'a' * 64, | |
| 33012345, 1790681952304657) | |
| self.assertFalse(failed) | |
| check_call.assert_not_called() | |
| class PresubmitSupportTest(unittest.TestCase): | |
| PKG = ('{"name": "x", "scripts": {"test": "a"}, ' | |
| '"devDependencies": {"typescript": "6.0.2"}}') | |
| def test_package_json_scripts_change_is_not_an_archive_input(self): | |
| new = self.PKG.replace('"test": "a"', '"test": "b"') | |
| self.assertEqual( | |
| manage_node_deps.archive_inputs_changed(['package.json'], self.PKG, | |
| new), []) | |
| def test_package_json_dependency_change_is_an_archive_input(self): | |
| new = self.PKG.replace('6.0.2', '6.0.3') | |
| self.assertEqual( | |
| manage_node_deps.archive_inputs_changed(['package.json'], self.PKG, | |
| new), ['package.json']) | |
| def test_lockfile_exclude_list_and_patches_are_archive_inputs(self): | |
| changed = [ | |
| 'DEPS', 'front_end/a.ts', 'package-lock.json', | |
| 'scripts/deps/npm_exclude.txt', | |
| 'scripts/deps/node_module_patches/karma-mocha.patch' | |
| ] | |
| self.assertEqual(manage_node_deps.archive_inputs_changed(changed), [ | |
| 'package-lock.json', | |
| 'scripts/deps/node_module_patches/karma-mocha.patch', | |
| 'scripts/deps/npm_exclude.txt' | |
| ]) | |
| def test_deps_entry_changed(self): | |
| old = DepsTest.DEPS | |
| self.assertFalse(manage_node_deps.deps_entry_changed(old, old)) | |
| self.assertTrue( | |
| manage_node_deps.deps_entry_changed( | |
| old, old.replace('1790681952304657', '1790681952304658'))) | |
| self.assertTrue(manage_node_deps.deps_entry_changed( | |
| 'deps = {}\n', old)) | |
| def test_check_deps_entry(self): | |
| self.assertEqual(manage_node_deps.check_deps_entry(DepsTest.DEPS), []) | |
| wrong_name = DepsTest.DEPS.replace( | |
| "'object_name': 'devtools-frontend/%s'" % ('a' * 64), | |
| "'object_name': 'devtools-frontend/latest'") | |
| self.assertEqual(manage_node_deps.check_deps_entry(wrong_name), [ | |
| "DEPS: 'node_modules' object_name must be devtools-frontend/%s " | |
| '(objects are named by their sha256).' % ('a' * 64) | |
| ]) | |
| self.assertEqual(manage_node_deps.check_deps_entry('deps = {}\n'), | |
| ["DEPS has no gcs entry named 'node_modules'."]) | |
| class ReferencesTest(unittest.TestCase): | |
| def setUp(self): | |
| tmpdir = tempfile.TemporaryDirectory() | |
| self.addCleanup(tmpdir.cleanup) | |
| self.root = tmpdir.name | |
| self.node_modules = os.path.join(self.root, 'node_modules') | |
| write( | |
| self.root, 'scripts/build/typescript.gni', """ | |
| inputs = [ | |
| "//node_modules/@types/node/index.d.ts", | |
| "../../node_modules/typescript/lib/tsc.js", | |
| ] | |
| node_modules_dir = "node_modules/" | |
| glob = "node_modules/*" | |
| """) | |
| write(self.root, 'scripts/tool.py', | |
| "p = path.join(root, 'node_modules/svgo/package.json')\n") | |
| # Another project's node_modules directory, not ours. | |
| write(self.root, 'extensions/e2e/runner.py', | |
| "x = 'extensions/e2e/node_modules/nested/index.js'\n") | |
| # Not scanned: wrong suffix, test fixtures, or skipped directories. | |
| write(self.root, 'front_end/a.ts', "import 'node_modules/ignored/x';") | |
| write(self.root, 'scripts/x_unittest.py', "'node_modules/fixture/y'") | |
| write(self.root, 'node_modules/x.py', "'node_modules/also/ignored'") | |
| write(self.root, 'third_party/x.gni', "'node_modules/not/scanned'") | |
| def test_collects_references(self): | |
| refs = manage_node_deps.referenced_node_module_paths(self.root) | |
| self.assertEqual(refs, { | |
| '@types/node/index.d.ts', 'typescript/lib/tsc.js', | |
| 'svgo/package.json' | |
| }) | |
| def test_verify_references(self): | |
| write(self.node_modules, '@types/node/index.d.ts') | |
| write(self.node_modules, 'typescript/lib/tsc.js') | |
| out = io.StringIO() | |
| with contextlib.redirect_stdout(out): | |
| failed = manage_node_deps.verify_references( | |
| self.node_modules, self.root) | |
| self.assertTrue(failed) | |
| self.assertIn('svgo/package.json', out.getvalue()) | |
| write(self.node_modules, 'svgo/package.json') | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| self.assertFalse( | |
| manage_node_deps.verify_references(self.node_modules, | |
| self.root)) | |
| @unittest.skipIf(shutil.which('rsync') is None, 'rsync is not installed') | |
| class FilterTest(unittest.TestCase): | |
| def setUp(self): | |
| tmpdir = tempfile.TemporaryDirectory() | |
| self.addCleanup(tmpdir.cleanup) | |
| self.tmp = tmpdir.name | |
| self.node_modules = os.path.join(self.tmp, 'node_modules') | |
| write(self.node_modules, 'pkg/package.json', '{}') | |
| write(self.node_modules, 'pkg/bin/pkg.js', '', 0o755) | |
| write(self.node_modules, 'pkg/index.d.ts') | |
| write(self.node_modules, 'pkg/src/index.ts') | |
| write(self.node_modules, 'pkg/README.md') | |
| write(self.node_modules, 'pkg/LICENSE.md') | |
| write(self.node_modules, 'pkg/test/pkg_test.js') | |
| write(self.node_modules, 'pkg/.github/workflows/ci.yml') | |
| write(self.node_modules, '.package-lock.json', '{}') | |
| symlink(self.node_modules, '.bin/pkg', '../pkg/bin/pkg.js') | |
| self.exclude = write( | |
| self.tmp, 'npm_exclude.txt', | |
| textwrap.dedent("""\ | |
| + /.bin/ | |
| + /.bin/*** | |
| + *.d.ts | |
| + LICENSE* | |
| */.* | |
| */test/ | |
| *.md | |
| *.ts | |
| """)) | |
| def test_filter_applies_rules_and_swaps_tree(self): | |
| staging = os.path.join(self.tmp, 'staging') | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.filter_node_modules( | |
| self.exclude, self.node_modules, staging) | |
| self.assertFalse(failed) | |
| self.assertFalse(os.path.exists(staging)) | |
| self.assertFalse(os.path.exists(self.node_modules + '.old')) | |
| remaining = set() | |
| for dirpath, _, filenames in os.walk(self.node_modules): | |
| for filename in filenames: | |
| remaining.add( | |
| os.path.relpath(os.path.join(dirpath, filename), | |
| self.node_modules)) | |
| self.assertEqual( | |
| remaining, { | |
| '.bin/pkg', '.package-lock.json', 'pkg/package.json', | |
| 'pkg/bin/pkg.js', 'pkg/index.d.ts', 'pkg/LICENSE.md' | |
| }) | |
| self.assertTrue( | |
| os.path.islink(os.path.join(self.node_modules, '.bin', 'pkg'))) | |
| self.assertTrue( | |
| os.access(os.path.join(self.node_modules, 'pkg/bin/pkg.js'), | |
| os.X_OK)) | |
| def test_filter_requires_exclude_file(self): | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| self.assertTrue( | |
| manage_node_deps.filter_node_modules( | |
| os.path.join(self.tmp, 'missing.txt'), self.node_modules, | |
| os.path.join(self.tmp, 'staging'))) | |
| self.assertTrue(os.path.isdir(self.node_modules)) | |
| def test_filter_stages_next_to_node_modules(self): | |
| # Staging must share node_modules' filesystem (os.rename fails across | |
| # devices), so the default is a sibling directory, not out/. | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.filter_node_modules( | |
| self.exclude, self.node_modules) | |
| self.assertFalse(failed) | |
| self.assertFalse(os.path.exists(self.node_modules + '.filtered')) | |
| self.assertFalse(os.path.exists(self.node_modules + '.old')) | |
| self.assertTrue( | |
| os.path.isfile(os.path.join(self.node_modules, 'pkg/index.d.ts'))) | |
| def test_filter_restores_tree_when_swap_fails(self): | |
| original = set() | |
| for dirpath, _, filenames in os.walk(self.node_modules): | |
| for filename in filenames: | |
| original.add( | |
| os.path.relpath(os.path.join(dirpath, filename), | |
| self.node_modules)) | |
| real_rename = os.rename | |
| def failing_rename(src, dst): | |
| if src.endswith('.filtered'): | |
| raise OSError(18, 'Invalid cross-device link') | |
| return real_rename(src, dst) | |
| with mock.patch.object(manage_node_deps.os, 'rename', failing_rename): | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.filter_node_modules( | |
| self.exclude, self.node_modules) | |
| self.assertTrue(failed) | |
| restored = set() | |
| for dirpath, _, filenames in os.walk(self.node_modules): | |
| for filename in filenames: | |
| restored.add( | |
| os.path.relpath(os.path.join(dirpath, filename), | |
| self.node_modules)) | |
| self.assertEqual(restored, original) | |
| self.assertFalse(os.path.exists(self.node_modules + '.old')) | |
| self.assertFalse(os.path.exists(self.node_modules + '.filtered')) | |
| def test_repo_exclude_list(self): | |
| """Executable summary of scripts/deps/npm_exclude.txt.""" | |
| exclude = os.path.join( | |
| os.path.dirname(os.path.abspath(manage_node_deps.__file__)), | |
| 'npm_exclude.txt') | |
| tree = os.path.join(self.tmp, 'repo_tree') | |
| kept = [ | |
| '.bin/pkg', | |
| '.package-lock.json', | |
| 'pkg/package.json', | |
| 'pkg/index.js', | |
| 'pkg/index.d.ts', | |
| 'pkg/types.d.mts', | |
| 'pkg/LICENSE', | |
| 'pkg/LICENSE.md', | |
| 'pkg/NOTICE', | |
| 'karma/static/context.html', | |
| 'typescript/lib/typescript.js', | |
| 'typescript/lib/lib.dom.d.ts', | |
| 'typescript/ThirdPartyNoticeText.txt', | |
| 'web-component-analyzer/node_modules/typescript/lib/typescript.js', | |
| 'web-component-analyzer/node_modules/typescript/lib/lib.dom.d.ts', | |
| 'devtools-protocol/types/protocol.d.ts', | |
| 'vscode-css-languageservice/lib/umd/cssLanguageService.js', | |
| 'vscode-html-languageservice/lib/umd/htmlLanguageService.js', | |
| ] | |
| dropped = [ | |
| 'pkg/README.md', | |
| 'pkg/CHANGELOG.md', | |
| 'pkg/index.js.map', | |
| 'pkg/src/index.ts', | |
| 'pkg/test/index_test.js', | |
| 'pkg/docs/api.html', | |
| 'pkg/.github/workflows/ci.yml', | |
| 'pkg/.eslintrc', | |
| 'pkg/logo.png', | |
| 'typescript/lib/fr/diagnosticMessages.generated.json', | |
| 'web-component-analyzer/node_modules/typescript/lib/tsc.js', | |
| 'web-component-analyzer/node_modules/typescript/lib/tsserver.js', | |
| 'web-component-analyzer/node_modules/typescript/lib/' | |
| 'tsserverlibrary.js', | |
| 'web-component-analyzer/node_modules/typescript/lib/' | |
| 'typingsInstaller.js', | |
| 'devtools-protocol/json/browser_protocol.json', | |
| 'devtools-protocol/pdl/browser_protocol.pdl', | |
| 'vscode-css-languageservice/lib/esm/cssLanguageService.js', | |
| 'vscode-html-languageservice/lib/esm/htmlLanguageService.js', | |
| 'node_modules.tar.gz', | |
| '.devtools-frontend_x_hash', | |
| '.clang-format', | |
| '.cache/eslint/x', | |
| ] | |
| for rel in kept + dropped: | |
| if rel == '.bin/pkg': | |
| symlink(tree, rel, '../pkg/index.js') | |
| else: | |
| write(tree, rel) | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| failed = manage_node_deps.filter_node_modules( | |
| exclude, tree, os.path.join(self.tmp, 'staging')) | |
| self.assertFalse(failed) | |
| remaining = set() | |
| for dirpath, _, filenames in os.walk(tree): | |
| for filename in filenames: | |
| remaining.add( | |
| os.path.relpath(os.path.join(dirpath, filename), tree)) | |
| self.assertEqual(remaining, set(kept)) | |
| class MainTest(unittest.TestCase): | |
| def setUp(self): | |
| for name in ('run_npm_command', 'filter_node_modules', | |
| 'verify_references', 'archive_node_modules', | |
| 'upload_archive', 'check_archive'): | |
| patcher = mock.patch.object(manage_node_deps, | |
| name, | |
| return_value=False) | |
| setattr(self, name, patcher.start()) | |
| self.addCleanup(patcher.stop) | |
| def test_default_installs_filters_and_archives(self): | |
| argv = ['--archive-path', '/tmp/x.tar.gz'] | |
| self.assertEqual(manage_node_deps.main(argv), 0) | |
| self.run_npm_command.assert_called_once() | |
| self.filter_node_modules.assert_called_once_with(None) | |
| self.verify_references.assert_called_once() | |
| self.archive_node_modules.assert_called_once_with('/tmp/x.tar.gz') | |
| self.upload_archive.assert_not_called() | |
| def test_install_failure_stops(self): | |
| self.run_npm_command.return_value = True | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| self.assertEqual(manage_node_deps.main([]), 1) | |
| self.filter_node_modules.assert_not_called() | |
| self.archive_node_modules.assert_not_called() | |
| def test_filter_failure_stops(self): | |
| self.filter_node_modules.return_value = True | |
| self.assertEqual(manage_node_deps.main(['--skip-install']), 1) | |
| self.verify_references.assert_not_called() | |
| self.archive_node_modules.assert_not_called() | |
| def test_missing_reference_stops(self): | |
| self.verify_references.return_value = True | |
| self.assertEqual(manage_node_deps.main(['--skip-install']), 1) | |
| self.archive_node_modules.assert_not_called() | |
| def test_skip_install_uses_given_exclude_file(self): | |
| argv = ['--skip-install', '--exclude-file', '/tmp/x.txt'] | |
| self.assertEqual(manage_node_deps.main(argv), 0) | |
| self.run_npm_command.assert_not_called() | |
| self.filter_node_modules.assert_called_once_with('/tmp/x.txt') | |
| self.archive_node_modules.assert_called_once() | |
| def test_upload_after_archive(self): | |
| argv = [ | |
| '--skip-install', '--upload', '--archive-path', '/tmp/x.tar.gz' | |
| ] | |
| self.assertEqual(manage_node_deps.main(argv), 0) | |
| self.archive_node_modules.assert_called_once_with('/tmp/x.tar.gz') | |
| self.upload_archive.assert_called_once_with('/tmp/x.tar.gz', None) | |
| def test_archive_error_stops_before_upload(self): | |
| self.archive_node_modules.side_effect = ValueError('empty') | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| self.assertEqual( | |
| manage_node_deps.main(['--skip-install', '--upload']), 1) | |
| self.upload_archive.assert_not_called() | |
| def test_check_only_compares(self): | |
| argv = [ | |
| '--check', '--archive-path', '/tmp/x.tar.gz', '--deps-file', | |
| '/tmp/DEPS' | |
| ] | |
| self.assertEqual(manage_node_deps.main(argv), 0) | |
| self.run_npm_command.assert_not_called() | |
| self.filter_node_modules.assert_not_called() | |
| self.check_archive.assert_called_once_with('/tmp/x.tar.gz', | |
| '/tmp/DEPS') | |
| self.archive_node_modules.assert_not_called() | |
| if __name__ == '__main__': | |
| unittest.main() |