chromium/tools/typescript/ts_library_test.py

#!/usr/bin/env python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import json
import ts_library
import ts_definitions
import path_mappings
import os
import shutil
import tempfile
import unittest

_HERE_DIR = os.path.dirname(__file__)
_CWD = os.getcwd()


class TsLibraryTest(unittest.TestCase):
  def setUp(self):
    self._out_folder = None
    self._additional_flags = []

  def tearDown(self):
    if self._out_folder:
      shutil.rmtree(self._out_folder)

  def _build_project1(self, enable_source_maps=False):
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project1')

    # Generate definition .d.ts file for legacy JS file.
    ts_definitions.main([
        '--root_dir',
        os.path.join(_HERE_DIR, 'tests', 'project1'),
        '--gen_dir',
        gen_dir,
        '--out_dir',
        gen_dir,
        '--js_files',
        'legacy_file.js',
    ])

    # Build project1, which includes a mix of TS and definition files.
    args = [
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project1'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'foo.ts',
        '--definitions',
        'legacy_file.d.ts',
        '--composite',
    ]

    if enable_source_maps:
      args += ['--enable_source_maps']

    ts_library.main(args)
    return gen_dir

  def _assert_project1_output(self, gen_dir):
    files = [
        'foo.d.ts',
        'foo.js',
        'legacy_file.d.ts',
        'tsconfig_definitions.json',
        'tsconfig_build_ts.json',
        'build_ts_manifest.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that the generated .tsbuildinfo file is deleted.
    tsbuildinfo = 'tsconfig_build_ts.tsbuildinfo'
    self.assertFalse(os.path.exists(os.path.join(gen_dir, tsbuildinfo)),
                     tsbuildinfo)

  # Builds project2 which depends on files from project1 and project3, project6,
  # both via relative URLs, as well as via absolute chrome:// and
  # chrome://resources/ URLs.
  def _build_project2(self, project1_gen_dir, project3_gen_dir,
                      project6_gen_dir):
    root_dir = os.path.join(_HERE_DIR, 'tests', 'project2')
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project2')
    project1_gen_dir = os.path.relpath(project1_gen_dir, gen_dir)
    project3_gen_dir = os.path.relpath(project3_gen_dir, gen_dir)
    project6_gen_dir = os.path.relpath(project6_gen_dir, gen_dir)
    # Using path mappings to generate the path map file. path_mappings is also
    # unit tested separately in path_mappings_test.py.
    path_mappings.main([
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--raw_deps',
        '//ui/webui/resources/js:build_ts',
        '--output_suffix',
        'project2',
    ])

    ts_library.main([
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(root_dir, _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar.ts',
        '--deps',
        os.path.join(project1_gen_dir, 'tsconfig_build_ts.json'),
        os.path.join(project3_gen_dir, 'tsconfig_build_ts.json'),
        os.path.join(project6_gen_dir, 'tsconfig_build_ts.json'),
        '--path_mappings',
        'chrome://some-other-source/*|' + os.path.join(project1_gen_dir, '*'),
        '--path_mappings_file',
        'path_mappings_project2.json',
        '--tsconfig_base',
        os.path.relpath(os.path.join(root_dir, 'tsconfig_base.json'), gen_dir),
    ])
    return gen_dir

  def _assert_project2_output(self, gen_dir):
    files = [
        'bar.js',
        'tsconfig_build_ts.json',
        'build_ts_manifest.json',
        'path_mappings_project2.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    non_existing_files = [
        'bar.d.ts',
        'tsconfig_build_ts.tsbuildinfo',
    ]
    for f in non_existing_files:
      self.assertFalse(os.path.exists(os.path.join(gen_dir, f)), f)

  # Builds project3, which includes only definition files.
  def _build_project3(self):
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project3')

    ts_library.main([
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project3'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--definitions',
        os.path.relpath(
            os.path.join(_HERE_DIR, 'tests', 'project3', 'baz.d.ts'), gen_dir),
        '--composite',
    ])
    return gen_dir

  def _assert_project3_output(self, gen_dir):
    self.assertTrue(
        os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.json')))
    self.assertFalse(
        os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.tsbuildinfo')))
    self.assertFalse(
        os.path.exists(os.path.join(gen_dir, 'build_ts_manifest.json')))

  def _build_project4(self):
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project4')

    # Build project4, which includes multiple TS files, only one of which should
    # be included in the manifest.
    ts_library.main([
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project4'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'include.ts',
        'exclude.ts',
        '--manifest_excludes',
        'exclude.ts',
    ])
    return gen_dir

  def _assert_project4_output(self, gen_dir):
    files = [
        'include.js',
        'exclude.js',
        'tsconfig_build_ts.json',
        'build_ts_manifest.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that the generated manifest file doesn't include exclude.js.
    manifest = os.path.join(gen_dir, 'build_ts_manifest.json')
    self._assert_manifest_files(manifest, ['include.js'])

  def _assert_manifest_files(self, manifest_path, expected_files):
    with open(manifest_path, 'r') as f:
      data = json.load(f)
      self.assertEqual(data['files'], expected_files)

  def _build_project5(self):
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project5')
    out_dir_test = os.path.join(self._out_folder, 'project5_test')

    # Build project5, which includes 2 TS projects one for prod and one for
    # test, it should generate different manifest, tsconfig and tsbuildinfo.
    # prod:
    ts_library.main([
        '--output_suffix',
        'build_ts',
        '--composite',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project5'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar.ts',
    ])

    # test:
    ts_library.main([
        '--output_suffix',
        'test_build_ts',
        '--deps',
        os.path.join(gen_dir, 'tsconfig_build_ts.json'),
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project5'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar_test.ts',
    ])

    return gen_dir

  def _assert_project5_output(self, gen_dir):
    # prod:
    self.assertTrue(
        os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.json')))
    manifest = os.path.join(gen_dir, 'build_ts_manifest.json')
    self.assertTrue(os.path.exists(manifest))
    self._assert_manifest_files(manifest, ['bar.js'])

    # test:
    self.assertTrue(
        os.path.exists(os.path.join(gen_dir, 'tsconfig_test_build_ts.json')))
    manifest_test = os.path.join(gen_dir, 'test_build_ts_manifest.json')
    self.assertTrue(os.path.exists(manifest_test))
    self._assert_manifest_files(manifest_test, ['bar_test.js'])

  def _build_project6(self):
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'ui', 'webui', 'resources', 'js')
    out_dir = os.path.join(self._out_folder, 'ui', 'webui', 'resources', 'tsc',
                           'js')

    # Build project6, which simulates the build setup and location of shared
    # ui/webui/resources/ projects, and is used to test the codepath that infers
    # |path_mappings| from |raw_deps|.
    ts_library.main([
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(
            os.path.join(_HERE_DIR, 'tests', 'ui', 'webui', 'resources', 'js'),
            _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(out_dir, _CWD),
        '--in_files',
        'assert.ts',
        '--composite',
    ])

    return (gen_dir, out_dir)

  def _assert_project6_output(self, gen_dir, out_dir):
    gen_dir_files = [
        'tsconfig_build_ts.json',
        'build_ts_manifest.json',
    ]
    for f in gen_dir_files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that the generated .tsbuildinfo file is deleted.
    tsbuildinfo = 'tsconfig_build_ts.tsbuildinfo'
    self.assertFalse(os.path.exists(os.path.join(gen_dir, tsbuildinfo)),
                     tsbuildinfo)

    out_dir_files = [
        'assert.d.ts',
        'assert.js',
    ]
    for f in out_dir_files:
      self.assertTrue(os.path.exists(os.path.join(out_dir, f)), f)


  # Test success case where both project1 and project2 are compiled successfully
  # and no errors are thrown.
  def testSuccess(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    project1_gen_dir = self._build_project1()
    self._assert_project1_output(project1_gen_dir)

    project3_gen_dir = self._build_project3()
    self._assert_project3_output(project3_gen_dir)

    (project6_gen_dir, project6_out_dir) = self._build_project6()
    self._assert_project6_output(project6_gen_dir, project6_out_dir)

    project2_gen_dir = self._build_project2(project1_gen_dir, project3_gen_dir,
                                            project6_gen_dir)
    self._assert_project2_output(project2_gen_dir)

    project4_gen_dir = self._build_project4()
    self._assert_project4_output(project4_gen_dir)

    project5_gen_dir = self._build_project5()
    self._assert_project5_output(project5_gen_dir)

  # Test error case where a type violation exists, ensure that an error is
  # thrown.
  def testError(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project1')
    try:
      ts_library.main([
          '--output_suffix',
          'build_ts',
          '--root_gen_dir',
          os.path.relpath(self._out_folder, gen_dir),
          '--root_src_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
          '--root_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project1'), _CWD),
          '--gen_dir',
          os.path.relpath(gen_dir, _CWD),
          '--out_dir',
          os.path.relpath(gen_dir, _CWD),
          '--in_files',
          'errors.ts',
          '--composite',
      ])
    except RuntimeError as err:
      self.assertTrue('Type \'number\' is not assignable to type \'string\'' \
                      in str(err))
      self.assertFalse(
          os.path.exists(os.path.join(gen_dir,
                                      'tsconfig_build_ts.tsbuildinfo')))
    else:
      self.fail('Failed to detect type error')

  # Test error case where the project's tsconfig file is failing validation.
  def testTsConfigValidationError(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    root_dir = os.path.join(_HERE_DIR, 'tests', 'project5')
    gen_dir = os.path.join(self._out_folder, 'tools', 'typescript', 'tests',
                           'project5')
    try:
      ts_library.main([
          '--output_suffix',
          'build_ts',
          '--root_gen_dir',
          os.path.relpath(self._out_folder, gen_dir),
          '--root_src_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
          '--root_dir',
          os.path.relpath(root_dir, _CWD),
          '--gen_dir',
          os.path.relpath(gen_dir, _CWD),
          '--out_dir',
          os.path.relpath(gen_dir, _CWD),
          '--in_files',
          'bar.ts',
          '--tsconfig_base',
          os.path.relpath(os.path.join(root_dir, 'tsconfig_base.json'),
                          gen_dir),
      ])
    except AssertionError as err:
      self.assertTrue(
          str(err).startswith('Invalid |composite| flag detected in '))
    else:
      self.fail('Failed to detect error')

  # Test case where |enable_source_maps| is specified.
  def testEnableSourceMaps(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)

    expectations_dir = os.path.join(_HERE_DIR, 'tests', 'expected', 'project1')

    # Build project1 which source maps enabled.
    gen_dir = self._build_project1(enable_source_maps=True)

    # Assert output is as expected.
    def _read_file(parent_dir, file_name):
      file_path = os.path.join(gen_dir, file_name)
      with open(file_path, 'r', newline='') as f:
        return f.read()

    actual_js = _read_file(gen_dir, 'foo.js')
    expected_js = _read_file(expectations_dir, 'foo.js')
    self.assertMultiLineEqual(expected_js, actual_js)


if __name__ == '__main__':
  unittest.main()