mzcompose.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. """Tests for the Materialize Fivetran destination.
  10. This composition is a lightweight test harness for the Materialize Fivetran
  11. destination.
  12. Each test is structured as a directory within whose name begins with `test-`.
  13. Each test directory may contain:
  14. * any number of Testdrive scripts, which are files whose name ends in `.td`
  15. * any number of Fivetran Destination Tester scripts, which are files whose
  16. name ends in `.json`
  17. * a README file, which must be named `00-README`
  18. The test harness boots Materialize and the Materialize Fivetran Destination
  19. server. At the start of each test, the harness creates a database named `test`
  20. that is owned by the `materialize` user. Then, the test harness runs each
  21. Testdrive and Fivetran Destination Tester script within the test directory in
  22. lexicographic order. If a script fails, the test is marked as failed and no
  23. further scripts from the test are executed.
  24. A script is normally considered to fail if Testdrive or the Fivetran Destination
  25. Tester exit with a non-zero code. However, if the last line of a Fivetran
  26. Destination Tester script matches the pattern `// FAIL: <message>`, the test
  27. harness will expect the Fivetran Destination Tester to exit with a non-zero code
  28. and with `<message>` printed to stdout; the test script will be marked as failed
  29. if it exits with code zero or if `<message>` is not printed to stdout.
  30. For details on Testdrive, consult doc/developer/testdrive.md.
  31. For details on the Fivetran Destination Tester, which is a tool provided by
  32. Fivetran, consult misc/fivetran-sdk/tools/README.md.
  33. To invoke the test harness locally:
  34. $ cd test/fivetran-destination
  35. $ ./mzcompose [--dev] run default -- [FILTER]
  36. The optional FILTER argument indicates a pattern which limits which test cases
  37. are run. A pattern matches a test case if the pattern is contained within the
  38. name of the test directory.
  39. """
  40. import shutil
  41. from pathlib import Path
  42. from materialize.mzcompose.composition import Composition, WorkflowArgumentParser
  43. from materialize.mzcompose.services.fivetran_destination import FivetranDestination
  44. from materialize.mzcompose.services.fivetran_destination_tester import (
  45. FivetranDestinationTester,
  46. )
  47. from materialize.mzcompose.services.materialized import Materialized
  48. from materialize.mzcompose.services.mz import Mz
  49. from materialize.mzcompose.services.testdrive import Testdrive
  50. ROOT = Path(__file__).parent
  51. SERVICES = [
  52. Mz(app_password=""),
  53. Materialized(),
  54. Testdrive(
  55. no_reset=True,
  56. default_timeout="5s",
  57. ),
  58. FivetranDestination(
  59. volumes_extra=["./data:/data"],
  60. ),
  61. FivetranDestinationTester(
  62. destination_host="fivetran-destination",
  63. destination_port=6874,
  64. volumes_extra=["./data:/data"],
  65. ),
  66. ]
  67. # Tests that are currently broken because the Fivetran Tester seems to do the wrong thing.
  68. BROKEN_TESTS = []
  69. def workflow_default(c: Composition, parser: WorkflowArgumentParser) -> None:
  70. parser.add_argument("filter", nargs="?")
  71. args = parser.parse_args()
  72. c.up("materialized", "fivetran-destination")
  73. for path in ROOT.iterdir():
  74. if path.name.startswith("test-"):
  75. if args.filter and args.filter not in path.name:
  76. print(f"Test case {path.name!r} does not match filter; skipping...")
  77. continue
  78. if path.name in BROKEN_TESTS:
  79. print(f"Test case {path.name!r} is currently broken; skipping...")
  80. continue
  81. with c.test_case(path.name):
  82. _run_test_case(c, path)
  83. def _run_test_case(c: Composition, path: Path):
  84. c.sql("DROP DATABASE IF EXISTS test")
  85. c.sql("CREATE DATABASE test")
  86. c.sql('DROP CLUSTER IF EXISTS "name with space" CASCADE')
  87. c.sql("CREATE CLUSTER \"name with space\" SIZE '1'")
  88. for test_file in sorted(p for p in path.iterdir()):
  89. test_file = test_file.relative_to(ROOT)
  90. if test_file.suffix == ".td":
  91. c.run_testdrive_files(str(test_file))
  92. elif test_file.suffix == ".json":
  93. _run_destination_tester(c, test_file)
  94. elif test_file.name != "00-README":
  95. raise RuntimeError(f"unexpected test file: {test_file}")
  96. # Run the Fivetran Destination Tester with a single file.
  97. def _run_destination_tester(c: Composition, test_file: Path):
  98. # The Fivetran Destination tester operates on an entire directory at a time. We run
  99. # individual test cases by copying everything into a single "data" directory which
  100. # automatically gets cleaned up at the start and end of every run.
  101. with DataDirGuard(ROOT / "data") as data_dir:
  102. test_file = ROOT / test_file
  103. shutil.copy(test_file, data_dir.path())
  104. last_line = test_file.read_text().splitlines()[-1]
  105. if last_line.startswith("// FAIL: "):
  106. expected_failure = last_line.removeprefix("// FAIL: ")
  107. else:
  108. expected_failure = None
  109. if expected_failure:
  110. ret = c.run("fivetran-destination-tester", check=False, capture=True)
  111. print("stdout:")
  112. print(ret.stdout)
  113. assert (
  114. ret.returncode != 0
  115. ), "destination tester returned success code when expected failure"
  116. # TODO(parkmycar): Re-enable this assertion when the Fivetran Destination Tester starts
  117. # outputting errors again.
  118. #
  119. # assert (
  120. # expected_failure in ret.stderr
  121. # ), f"destination tester did not fail with expected message {expected_failure!r}\n\tfound: {ret.stdout!r}"
  122. else:
  123. c.run("fivetran-destination-tester")
  124. # Type that implements the Context Protocol that makes it easy to automatically clean up our data
  125. # directory before and after every test run.
  126. class DataDirGuard:
  127. def __init__(self, dir: Path):
  128. self._dir = dir
  129. def clean(self):
  130. for file in self._dir.iterdir():
  131. if file.name in ("configuration.json", ".gitignore"):
  132. continue
  133. file.unlink()
  134. def path(self) -> Path:
  135. return self._dir
  136. def __enter__(self):
  137. self.clean()
  138. return self
  139. def __exit__(self, exc_type, exc_value, traceback):
  140. self.clean()