Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

157 lines
5.7KB

  1. import argparse
  2. import multiprocessing
  3. import pytest
  4. from pathlib import Path
  5. from concurrent import futures
  6. import sys
  7. from typing import NoReturn, Sequence, Optional
  8. from typing_extensions import Protocol
  9. import subprocess
  10. from . import paths, toolchain
  11. from .dds import DDSWrapper
  12. from .bootstrap import BootstrapMode, get_bootstrap_exe
  13. def make_argparser() -> argparse.ArgumentParser:
  14. """Create an argument parser for the dds-ci command-line"""
  15. parser = argparse.ArgumentParser()
  16. parser.add_argument('-B',
  17. '--bootstrap-with',
  18. help='How are we to obtain a bootstrapped DDS executable?',
  19. metavar='{download,build,skip,lazy}',
  20. type=BootstrapMode,
  21. default=BootstrapMode.Lazy)
  22. parser.add_argument('--rapid', help='Run CI for fast development iterations', action='store_true')
  23. parser.add_argument('--test-toolchain',
  24. '-TT',
  25. type=Path,
  26. metavar='<toolchain-file>',
  27. help='The toolchain to use for the first build, which will be passed through the tests')
  28. parser.add_argument('--main-toolchain',
  29. '-T',
  30. type=Path,
  31. dest='toolchain',
  32. metavar='<toolchain-file>',
  33. help='The toolchain to use for the final build')
  34. parser.add_argument('--jobs',
  35. '-j',
  36. type=int,
  37. help='Number of parallel jobs to use when building and testing',
  38. default=multiprocessing.cpu_count() + 2)
  39. parser.add_argument('--build-only', action='store_true', help='Only build the dds executable, do not run tests')
  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_test_toolchain()
  73. build_dir = paths.BUILD_DIR / '_ci-test'
  74. with toolchain.fixup_toolchain(test_tc) as new_tc:
  75. dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=build_dir, jobs=args.jobs)
  76. return DDSWrapper(build_dir / ('dds' + paths.EXE_SUFFIX))
  77. def run_pytest(dds: DDSWrapper, args: CommandArguments) -> int:
  78. """
  79. Execute pytest, testing against the given 'test_dds' executable. Returns
  80. the exit code of pytest.
  81. """
  82. basetemp = Path('/tmp/dds-ci')
  83. basetemp.mkdir(exist_ok=True, parents=True)
  84. return pytest.main([
  85. '-v',
  86. '--durations=10',
  87. '-n',
  88. str(args.jobs),
  89. f'--basetemp={basetemp}',
  90. f'--dds-exe={dds.path}',
  91. str(paths.PROJECT_ROOT / 'tests/'),
  92. ])
  93. def main_build(dds: DDSWrapper, args: CommandArguments) -> int:
  94. """
  95. Execute the main build of dds using the given 'dds' executable to build itself.
  96. """
  97. main_tc = args.toolchain or (
  98. # If we are in rapid-dev mode, use the test toolchain, which had audit/debug enabled
  99. toolchain.get_default_toolchain() if not args.rapid else toolchain.get_default_test_toolchain())
  100. with toolchain.fixup_toolchain(main_tc) as new_tc:
  101. try:
  102. dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=paths.BUILD_DIR, jobs=args.jobs)
  103. except subprocess.CalledProcessError as e:
  104. if args.rapid:
  105. return e.returncode
  106. raise
  107. return 0
  108. def ci_with_dds(dds: DDSWrapper, args: CommandArguments) -> int:
  109. """
  110. Execute CI using the given prior 'dds' executable.
  111. """
  112. if args.clean:
  113. dds.clean(build_dir=paths.BUILD_DIR)
  114. dds.catalog_json_import(paths.PROJECT_ROOT / 'old-catalog.json')
  115. pool = futures.ThreadPoolExecutor()
  116. test_fut = pool.submit(lambda: 0)
  117. if args.do_test and not args.rapid:
  118. test_dds = test_build(dds, args)
  119. test_fut = pool.submit(lambda: run_pytest(test_dds, args))
  120. main_fut = pool.submit(lambda: main_build(dds, args))
  121. for fut in futures.as_completed({test_fut, main_fut}):
  122. if fut.result():
  123. return fut.result()
  124. return 0
  125. def main(argv: Sequence[str]) -> int:
  126. args = parse_argv(argv)
  127. with get_bootstrap_exe(args.bootstrap_with) as f:
  128. return ci_with_dds(f, args)
  129. def start() -> NoReturn:
  130. sys.exit(main(sys.argv[1:]))
  131. if __name__ == "__main__":
  132. start()