You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
6.3KB

  1. import argparse
  2. import multiprocessing
  3. import pytest
  4. from pathlib import Path
  5. from concurrent import futures
  6. import shutil
  7. import sys
  8. from typing import NoReturn, Sequence, Optional
  9. from typing_extensions import Protocol
  10. import subprocess
  11. from . import paths, toolchain
  12. from .dds import DDSWrapper
  13. from .bootstrap import BootstrapMode, get_bootstrap_exe
  14. def make_argparser() -> argparse.ArgumentParser:
  15. """Create an argument parser for the dds-ci command-line"""
  16. parser = argparse.ArgumentParser()
  17. parser.add_argument('-B',
  18. '--bootstrap-with',
  19. help='How are we to obtain a bootstrapped DDS executable?',
  20. metavar='{download,build,skip,lazy}',
  21. type=BootstrapMode,
  22. default=BootstrapMode.Lazy)
  23. parser.add_argument('--rapid', help='Run CI for fast development iterations', action='store_true')
  24. parser.add_argument('--test-toolchain',
  25. '-TT',
  26. type=Path,
  27. metavar='<toolchain-file>',
  28. help='The toolchain to use for the first build, which will be passed through the tests')
  29. parser.add_argument('--main-toolchain',
  30. '-T',
  31. type=Path,
  32. dest='toolchain',
  33. metavar='<toolchain-file>',
  34. help='The toolchain to use for the final build')
  35. parser.add_argument('--jobs',
  36. '-j',
  37. type=int,
  38. help='Number of parallel jobs to use when building and testing',
  39. default=multiprocessing.cpu_count() + 2)
  40. parser.add_argument('--clean', action='store_true', help="Don't remove prior build/deps results")
  41. parser.add_argument('--no-test',
  42. action='store_false',
  43. dest='do_test',
  44. help='Skip testing and just build the final result')
  45. return parser
  46. class CommandArguments(Protocol):
  47. """
  48. The result of parsing argv with the dds-ci argument parser.
  49. """
  50. #: Whether the user wants us to clean result before building
  51. clean: bool
  52. #: The bootstrap method the user has requested
  53. bootstrap_with: BootstrapMode
  54. #: The toolchain to use when building the 'dds' executable that will be tested.
  55. test_toolchain: Optional[Path]
  56. #: The toolchain to use when building the main 'dds' executable to publish
  57. toolchain: Optional[Path]
  58. #: The maximum number of parallel jobs for build and test
  59. jobs: int
  60. #: Whether we should run the pytest tests
  61. do_test: bool
  62. #: Rapid-CI is for 'dds' development purposes
  63. rapid: bool
  64. def parse_argv(argv: Sequence[str]) -> CommandArguments:
  65. """Parse the given dds-ci command-line argument list"""
  66. return make_argparser().parse_args(argv)
  67. def test_build(dds: DDSWrapper, args: CommandArguments) -> DDSWrapper:
  68. """
  69. Execute the build that generates the test-mode executable. Uses the given 'dds'
  70. to build the new dds. Returns a DDSWrapper around the generated test executable.
  71. """
  72. test_tc = args.test_toolchain or toolchain.get_default_audit_toolchain()
  73. print(f'Test build is building with toolchain: {test_tc}')
  74. build_dir = paths.BUILD_DIR
  75. with toolchain.fixup_toolchain(test_tc) as new_tc:
  76. dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=build_dir, jobs=args.jobs, timeout=60 * 15)
  77. return DDSWrapper(build_dir / ('dds' + paths.EXE_SUFFIX))
  78. def run_pytest(dds: DDSWrapper, args: CommandArguments) -> int:
  79. """
  80. Execute pytest, testing against the given 'test_dds' executable. Returns
  81. the exit code of pytest.
  82. """
  83. basetemp = Path('/tmp/dds-ci')
  84. basetemp.mkdir(exist_ok=True, parents=True)
  85. return pytest.main([
  86. '-v',
  87. '--durations=10',
  88. '-n',
  89. str(args.jobs),
  90. f'--basetemp={basetemp}',
  91. f'--dds-exe={dds.path}',
  92. f'--junit-xml={paths.BUILD_DIR}/pytest-junit.xml',
  93. str(paths.PROJECT_ROOT / 'tests/'),
  94. ])
  95. def main_build(dds: DDSWrapper, args: CommandArguments) -> int:
  96. """
  97. Execute the main build of dds using the given 'dds' executable to build itself.
  98. """
  99. main_tc = args.toolchain or (
  100. # If we are in rapid-dev mode, use the test toolchain, which had audit/debug enabled
  101. toolchain.get_default_toolchain() if not args.rapid else toolchain.get_default_audit_toolchain())
  102. print(f'Building with toolchain: {main_tc}')
  103. with toolchain.fixup_toolchain(main_tc) as new_tc:
  104. try:
  105. dds.build(toolchain=new_tc,
  106. root=paths.PROJECT_ROOT,
  107. build_root=paths.BUILD_DIR,
  108. jobs=args.jobs,
  109. timeout=60 * 15)
  110. except subprocess.CalledProcessError as e:
  111. if args.rapid:
  112. return e.returncode
  113. raise
  114. return 0
  115. def ci_with_dds(dds: DDSWrapper, args: CommandArguments) -> int:
  116. """
  117. Execute CI using the given prior 'dds' executable.
  118. """
  119. if args.clean:
  120. dds.clean(build_dir=paths.BUILD_DIR)
  121. dds.repo_add('https://repo-1.dds.pizza')
  122. if args.rapid:
  123. return main_build(dds, args)
  124. pool = futures.ThreadPoolExecutor()
  125. test_fut = pool.submit(lambda: 0)
  126. if args.do_test:
  127. # Build the test executable:
  128. test_dds = test_build(dds, args)
  129. # Move the generated exe and start tests. We'll start building the main
  130. # EXE and don't want to overwrite the test one while the tests are running
  131. dds_cp = paths.BUILD_DIR / ('dds.test' + paths.EXE_SUFFIX)
  132. test_dds.path.rename(dds_cp)
  133. test_dds.path = dds_cp
  134. # Workaround: dds doesn't rebuild the test-driver on toolchain changes:
  135. shutil.rmtree(paths.BUILD_DIR / '_test-driver')
  136. test_fut = pool.submit(lambda: run_pytest(test_dds, args))
  137. main_fut = pool.submit(lambda: main_build(dds, args))
  138. for fut in futures.as_completed({test_fut, main_fut}):
  139. if fut.result():
  140. return fut.result()
  141. return 0
  142. def main(argv: Sequence[str]) -> int:
  143. args = parse_argv(argv)
  144. with get_bootstrap_exe(args.bootstrap_with) as f:
  145. return ci_with_dds(f, args)
  146. def start() -> NoReturn:
  147. sys.exit(main(sys.argv[1:]))
  148. if __name__ == "__main__":
  149. start()