[fidldev] Move fidldev from fuchsia.git
Also fixes tests, and updates paths in the README
Change-Id: Ia98c7fa04979e6f899ac055c1516f7dba49ed4a9
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/392739
Reviewed-by: Mitchell Kember <mkember@google.com>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..01589f0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.pyc
+__pycache__
+
diff --git a/fidldev/OWNERS b/fidldev/OWNERS
new file mode 100644
index 0000000..aefdd75
--- /dev/null
+++ b/fidldev/OWNERS
@@ -0,0 +1,11 @@
+apang@google.com
+bprosnitz@google.com
+fcz@google.com
+godtamit@google.com
+ianloic@google.com
+mkember@google.com
+pascallouis@google.com
+peterjohnston@google.com
+yifeit@google.com
+
+# COMPONENT: FIDL
diff --git a/fidldev/README.md b/fidldev/README.md
new file mode 100644
index 0000000..03e6bcf
--- /dev/null
+++ b/fidldev/README.md
@@ -0,0 +1,25 @@
+# fidldev
+
+`fidldev` is a FIDL development workflow tool. Its goal is to automate
+repetitive processes while working on FIDL code, like running tests based on
+changed files, and regenerating golden files. It is also meant to be the
+source of truth for FIDL code locations and tests/regen commands.
+
+## Running fidldev:
+
+ $FIDLMISC_DIR/fidldev/fidldev.py --help
+
+This can be aliased for convenienced:
+
+ alias fidldev=$FIDLMISC_DIR/fidldev/fidldev.py
+
+Note that `fidldev` should be run from a valid repo (e.g. fuchsia.git, topaz,
+or third_party/go) in order to correctly detect changes, similar to
+`fx format-code`.
+
+## Testing fidldev:
+
+ python3 $FIDLMISC_DIR/fidldev/fidldev_test.py -b
+
+The `-b` flag will separate stdout output when printing test results. It can
+be removed when using debugging print statements in the test itself.
diff --git a/fidldev/env.py b/fidldev/env.py
new file mode 100644
index 0000000..f34ecfc
--- /dev/null
+++ b/fidldev/env.py
@@ -0,0 +1,24 @@
+"""
+Constants that come from the environment. These are in a separate module so
+that they can be mocked by tests if necessary.
+"""
+import os
+from pathlib import Path
+import sys
+
+FUCHSIA_DIR = Path(os.environ["FUCHSIA_DIR"])
+assert FUCHSIA_DIR.exists()
+
+with open(FUCHSIA_DIR / ".fx-build-dir") as f:
+ BUILD_DIR = f.read().strip()
+
+if sys.platform.startswith('linux'):
+ PLATFORM = 'linux'
+elif sys.platform == 'darwin':
+ PLATFORM = 'mac'
+else:
+ print("Unsupported platform: " + sys.platform)
+ sys.exit(1)
+
+with open(FUCHSIA_DIR / (BUILD_DIR + '.zircon') / 'args.gn') as f:
+ MODE = 'asan' if 'asan' in f.read() else 'clang'
diff --git a/fidldev/fidldev.py b/fidldev/fidldev.py
new file mode 100755
index 0000000..3bd6c06
--- /dev/null
+++ b/fidldev/fidldev.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import enum
+import subprocess
+import sys
+
+import regen
+import test_
+import util
+
+EXAMPLES = """
+Examples:
+
+Regen goldens and checked in bindings based on changed files in the current
+repo
+
+ fidldev regen
+
+Explicitly specify regen scripts:
+
+ fidldev regen fidlc fidlgen_dart
+ fidldev regen all
+
+Check which regen commands should be run:
+
+ fidldev regen --dry-run --no-build
+
+Run tests based on changed files in the current repo:
+
+ fidldev test
+
+Explicitly specify tests:
+
+ fidldev test fidlc hlcpp llcpp c
+
+Interactively filter test targets:
+
+ fidldev test --interactive
+
+Check which tests should be run:
+
+ fidldev test --dry-run --no-build --no-regen
+
+Pass flags to invocations of fx test:
+
+ fidldev test --fx-test-args "-v -o --dry"
+
+"""
+
+
+def test(args):
+ success = True
+ if args.targets:
+ if not args.no_regen:
+ util.print_warning(
+ 'explicit test targets provided, skipping regen...')
+ success = test_.test_explicit(
+ args.targets, not args.no_build, args.dry_run, args.interactive,
+ args.fx_test_args)
+ else:
+ changed_files = util.get_changed_files()
+ if not args.no_regen:
+ regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+ changed_files = util.get_changed_files()
+ success = test_.test_changed(
+ changed_files, not args.no_build, args.dry_run, args.interactive,
+ args.fx_test_args)
+ if args.dry_run:
+ print_dryrun_warning()
+ if not success:
+ sys.exit(1)
+
+
+def regen_cmd(args):
+ if args.targets:
+ regen.regen_explicit(args.targets, not args.no_build, args.dry_run)
+ else:
+ changed_files = util.get_changed_files()
+ regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+ if args.dry_run:
+ print_dryrun_warning()
+
+
+def print_dryrun_warning():
+ print(
+ 'NOTE: dry run is conservative and assumes that regen will '
+ 'always change files. If goldens do not change during an actual '
+ 'run, fewer tests/regen scripts may be run.')
+
+
+parser = argparse.ArgumentParser(
+ description="FIDL development workflow tool",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=EXAMPLES)
+subparsers = parser.add_subparsers()
+
+test_parser = subparsers.add_parser("test", help="Test your FIDL changes")
+test_parser.set_defaults(func=test)
+test_targets = [name for (name, _) in test_.TEST_GROUPS] + ['all']
+test_parser.add_argument(
+ 'targets',
+ metavar='target',
+ nargs='*',
+ help=
+ "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to test based on changed files"
+ .format(test_targets))
+test_parser.add_argument(
+ "--dry-run",
+ "-n",
+ help="Print out test commands without running",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--no-build",
+ "-b",
+ help="Don't rebuild targets used for testing",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--no-regen",
+ "-r",
+ help="Don't regen goldens before running tests",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--interactive",
+ "-i",
+ help="Interactively filter tests to be run",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--fx-test-args",
+ "-t",
+ help=
+ "Extra flags and arguments to pass to any invocations of fx test. The flag value is passed verbatim. By default, only '-v' is used.",
+ default='-v',
+)
+
+regen_parser = subparsers.add_parser("regen", help="Run regen commands")
+regen_parser.set_defaults(func=regen_cmd)
+regen_targets = [name for (name, _) in regen.REGEN_TARGETS] + ['all']
+regen_parser.add_argument(
+ 'targets',
+ metavar='target',
+ nargs='*',
+ help=
+ "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to regen based on changed files"
+ .format(regen_targets))
+regen_parser.add_argument(
+ "--dry-run",
+ "-n",
+ help="Print out commands without running them",
+ action="store_true",
+)
+regen_parser.add_argument(
+ "--no-build",
+ "-b",
+ help="Don't rebuild targets used for regen",
+ action="store_true",
+)
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ args.func(args)
diff --git a/fidldev/fidldev_test.py b/fidldev/fidldev_test.py
new file mode 100644
index 0000000..0dd557a
--- /dev/null
+++ b/fidldev/fidldev_test.py
@@ -0,0 +1,414 @@
+import itertools
+import os
+import unittest
+
+from test_util import get_commands, MOCK_FUCHSIA_DIR, MOCK_BUILD_DIR
+import regen
+import util
+
+
+# Many mocks contain 3 return values, because the regen script can call
+# get_changed_files up to 3 times:
+# 1. get the initial set of changed files
+# 2. get changed files after fidlc regen, to check if fidlgen needs regen
+# 3. get changed files after fidlgen regen, to check if go needs regen
+class TestFidlDevRegen(unittest.TestCase):
+
+ def test_basic_regen(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['zircon/tools/fidl/lib/flat_ast.cc']),
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN), util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN)
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_tables_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_go_goldens_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden',
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.go.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_rust_goldens_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden',
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.rs.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_go_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['tools/fidl/fidlgen_go/ir/ir.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(
+ ['tools/fidl/fidlgen_syzkaller/templates/struct.tmpl.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_dart_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['topaz/bin/fidlgen_dart/fidlgen_dart.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_regen_all(self):
+ command = ['regen', 'all']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+ def test_regen_no_build(self):
+ command = ['regen', 'all', '--no-build']
+ expected = [
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+ def test_regen_fidlgen(self):
+ command = ['regen', 'fidlgen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+
+class TestFidlDevTest(unittest.TestCase):
+
+ def test_no_changes(self):
+ mocks = {'get_changed_files': [[]]}
+ command = ['test', '--no-regen']
+ self.assertListEqual(get_commands(mocks, command), [])
+
+ def test_fidlc_changed(self):
+ mocks = {'get_changed_files': [['zircon/tools/fidl/lib/parser.cc']]}
+ command = ['test', '--no-regen']
+ expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed_zircon(self):
+ mocks = {
+ 'get_changed_files':
+ [['zircon/tools/fidl/goldens/bits.test.json.golden']]
+ }
+ command = ['test', '--no-regen']
+ expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'garnet/go/src/fidl/compiler/backend/goldens/struct.test.json',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_util_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'garnet/go/src/fidl/compiler/backend/types/types.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_backend_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'tools/fidl/fidlgen_rust/templates/enum.tmpl.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_golden_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.cc.golden',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS) | {util.HLCPP_TEST_TARGET}
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_dart_changed(self):
+ mocks = {
+ 'get_changed_files': [[
+ 'topaz/bin/fidlgen_dart/backend/ir/ir.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {util.FIDLGEN_DART_TEST_TARGET}
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_dart_golden_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'topaz/bin/fidlgen_dart/goldens/handles.test.json_async.dart.golden',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.DART_TEST_TARGET,
+ util.FIDLGEN_DART_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_c_runtime_changed(self):
+ mocks = {
+ 'get_changed_files': [[
+ 'zircon/system/ulib/fidl/txn_header.c',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.HLCPP_TEST_TARGET,
+ util.LLCPP_TEST_TARGET,
+ util.C_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_coding_tables_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'zircon/tools/fidl/goldens/union.test.tables.c.golden',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.HLCPP_TEST_TARGET,
+ util.LLCPP_TEST_TARGET,
+ util.C_TEST_TARGET,
+ }
+ self.assertEqual(actual[1], util.TEST_FIDLC)
+ self.assertTestsRun(actual[2], expected)
+
+ def test_go_runtime_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'third_party/go/src/syscall/zx/fidl/encoding_new.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.GO_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_dart_runtime_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'topaz/public/dart/fidl/lib/src/types.dart',
+ 'topaz/public/dart/fidl/lib/src/message.dart',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {util.DART_TEST_TARGET}
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_gidl_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'tools/fidl/gidl/main.go ',
+ 'tools/fidl/gidl/rust/benchmarks.go',
+ 'tools/fidl/gidl/rust/conformance.go',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.GIDL_TEST_TARGET,
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+ util.LLCPP_CONFORMANCE_TEST_TARGET,
+ util.RUST_CONFORMANCE_TEST_TARGET,
+ util.DART_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def assertTestsRun(self, raw_command, expected):
+ self.assertEqual(raw_command[0], 'fx')
+ self.assertEqual(raw_command[1], 'test')
+ self.assertEqual(raw_command[2], '-v')
+ tests = set(raw_command[3:])
+ self.assertSetEqual(tests, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/fidldev/regen.py b/fidldev/regen.py
new file mode 100644
index 0000000..55dc63d
--- /dev/null
+++ b/fidldev/regen.py
@@ -0,0 +1,106 @@
+import os
+
+import util
+
+FIDLC_REGEN = 'zircon/tools/fidl/testdata/regen.sh'
+FIDLGEN_REGEN = 'garnet/go/src/fidl/compiler/backend/typestest/regen.sh'
+FIDLGEN_DART_REGEN = 'topaz/bin/fidlgen_dart/regen.sh'
+GO_BINDINGS_REGEN = 'third_party/go/regen-fidl'
+
+
+def regen_fidlc_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLC, dry_run, exit_on_failure=True)
+ util.run(path_to_regen_command(FIDLC_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgen_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLGEN, dry_run, exit_on_failure=True)
+ util.run(
+ path_to_regen_command(FIDLGEN_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgendart_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLGEN_DART, dry_run, exit_on_failure=True)
+ util.run(
+ path_to_regen_command(FIDLGEN_DART_REGEN),
+ dry_run,
+ exit_on_failure=True)
+
+
+def regen_go_bindings(build_first, dry_run, exit_on_failure=True):
+ util.run(
+ path_to_regen_command(GO_BINDINGS_REGEN), dry_run, exit_on_failure=True)
+
+
+def path_to_regen_command(path):
+ return ['fx', 'exec', os.path.join(util.FUCHSIA_DIR, path)]
+
+
+def is_ir_changed():
+ for path in util.get_changed_files():
+ if path.startswith(
+ util.FIDLC_DIR) and path.endswith('.test.json.golden'):
+ return True
+ return False
+
+
+def is_go_bindings_changed():
+ for path in util.get_changed_files():
+ if path.startswith(
+ util.FIDLGEN_DIR) and path.endswith('.test.json.go.golden'):
+ return True
+ return False
+
+
+REGEN_TARGETS = [
+ ('fidlc', regen_fidlc_goldens),
+ ('fidlgen', regen_fidlgen_goldens),
+ ('fidlgen_dart', regen_fidlgendart_goldens),
+ ('go', regen_go_bindings),
+]
+
+
+def regen_explicit(targets, build_first, dry_run):
+ for target, func in REGEN_TARGETS:
+ if target in targets or 'all' in targets:
+ func(build_first, dry_run)
+
+
+def regen_changed(changed_files, build_first, dry_run):
+ regen_fidlc = False
+ regen_fidlgen = False
+ regen_fidlgendart = False
+ regen_go = False
+ for file_ in changed_files:
+ if file_.startswith(util.FIDLC_DIR):
+ regen_fidlc = True
+ if file_.startswith(util.FIDLGEN_DIR) or is_in_fidlgen_backend(file_):
+ regen_fidlgen = True
+ if file_.startswith(util.FIDLGEN_DART_DIR):
+ regen_fidlgendart = True
+ if file_.startswith(util.FIDLGEN_GO_DIR):
+ regen_go = True
+
+ if regen_fidlc:
+ regen_fidlc_goldens(build_first, dry_run)
+ if dry_run or is_ir_changed():
+ regen_fidlgen = True
+ regen_fidlgendart = True
+
+ if regen_fidlgen:
+ regen_fidlgen_goldens(build_first, dry_run)
+ if dry_run or is_go_bindings_changed():
+ regen_go = True
+
+ if regen_fidlgendart:
+ regen_fidlgendart_goldens(build_first, dry_run)
+
+ if regen_go:
+ regen_go_bindings(build_first, dry_run)
+
+
+def is_in_fidlgen_backend(path):
+ return any(path.startswith(b) for b in util.FIDLGEN_BACKEND_DIRS)
diff --git a/fidldev/test_.py b/fidldev/test_.py
new file mode 100644
index 0000000..ba438b7
--- /dev/null
+++ b/fidldev/test_.py
@@ -0,0 +1,201 @@
+import functools
+import pprint
+import util
+import regen
+
+
+def startswith(prefix):
+ return lambda s: s.startswith(prefix)
+
+
+def endswith(suffix):
+ return lambda s: s.endswith(suffix)
+
+
+# List of test groups. Each test group is of the following structure:
+# (NAME, (PREDICATES, TARGETS, BUILD COMMAND))
+# where:
+# - NAME is a name for the group of tests. This name is used to explicitly
+# invoke this test group on the command line (e.g. fidldev test foo would call
+# fx test on the TARGETS for group foo)
+# - PREDICATES is a list of predicates P such that P(file) returns true if the
+# test group should run when |file| is changed
+# - TARGETS is a list of test names as supported by `fx test`, e.g.
+# fully-formed Fuchsia Package URLs, package names, or directories
+# - BUILD COMMAND is any command that should be run prior to running the test
+# group. It can be None if no build step is required, and is skipped if the
+# --no-build flag is passed in. It currently needs to be a List - run_tests
+# needs to be updated to support strings
+TEST_GROUPS = [
+ (
+ 'fidlc', (
+ [startswith('zircon/tools/fidl')], [util.TEST_FIDLC],
+ util.BUILD_FIDLC_TESTS)),
+
+ # it's possible to be more selective on which changes map to which tests,
+ # but since fidlgen tests are fast to build and run, just do a blanket
+ # check.
+ (
+ 'fidlgen', (
+ [startswith(util.FIDLGEN_DIR)] +
+ [startswith(p) for p in util.FIDLGEN_BACKEND_DIRS],
+ util.FIDLGEN_TEST_TARGETS, util.BUILD_FIDLGEN)),
+ (
+ 'fidlgen_dart', (
+ [startswith(util.FIDLGEN_DART_DIR)],
+ [util.FIDLGEN_DART_TEST_TARGET], util.BUILD_FIDLGEN_DART)),
+ (
+ 'hlcpp', (
+ [
+ endswith('test.json.cc.golden'),
+ endswith('test.json.h.golden'),
+ endswith('test.tables.c.golden'),
+ startswith(util.HLCPP_RUNTIME),
+ startswith(util.C_RUNTIME),
+ ], [util.HLCPP_TEST_TARGET], None)),
+ (
+ 'llcpp', (
+ [
+ endswith('test.json.llcpp.cc.golden'),
+ endswith('test.json.llcpp.h.golden'),
+ endswith('test.tables.c.golden'),
+ startswith(util.LLCPP_RUNTIME),
+ startswith(util.C_RUNTIME)
+ ], [util.LLCPP_TEST_TARGET], None)),
+ (
+ 'c',
+ (
+ # normally, changes to the generated bindings are detected by looking at the
+ # goldens. Since we can't do this for C, we look at the coding table goldens
+ # and the c_generator instead.
+ [
+ endswith('test.tables.c.golden'),
+ startswith('zircon/tools/fidl/include/fidl/c_generator.h'),
+ startswith('zircon/tools/fidl/lib/c_generator.cc'),
+ startswith(util.C_RUNTIME),
+ ],
+ # NOTE: fidl-test should also run, but this script only supports component
+ # tests
+ [util.C_TEST_TARGET],
+ None)),
+ (
+ 'go', (
+ [endswith('test.json.go.golden'),
+ startswith(util.GO_RUNTIME)],
+ [util.GO_TEST_TARGET, util.GO_CONFORMANCE_TEST_TARGET], None)),
+ (
+ 'rust', (
+ [endswith('test.json.rs.golden'),
+ startswith(util.RUST_RUNTIME)], [util.RUST_TEST_TARGET], None)),
+ (
+ 'dart', (
+ [
+ endswith('test.json_async.dart.golden'),
+ endswith('test.json_test.dart.golden'),
+ startswith(util.DART_RUNTIME)
+ ], [util.DART_TEST_TARGET], None)),
+ (
+ 'gidl',
+ (
+ [startswith('tools/fidl/gidl')],
+ [
+ util.GIDL_TEST_TARGET,
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+ util.LLCPP_CONFORMANCE_TEST_TARGET,
+ util.RUST_CONFORMANCE_TEST_TARGET,
+ # dart conformance is bundled into the rest of the tests
+ util.DART_TEST_TARGET
+ ],
+ None)),
+]
+
+
+def test_explicit(targets, build_first, dry_run, interactive, fx_test_args):
+ """ Test an explicit set of test groups """
+ tests = []
+ for name, test in TEST_GROUPS:
+ if name in targets or 'all' in targets:
+ tests.append(test)
+ return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def test_changed(
+ changed_files, build_first, dry_run, interactive, fx_test_args):
+ """ Test relevant test groups given a set of changed files """
+ tests = []
+ for _, test in TEST_GROUPS:
+ (predicates, _, _) = test
+ for file_ in changed_files:
+ if any(p(file_) for p in predicates):
+ tests.append(test)
+ return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def run_tests(tests, build_first, dry_run, interactive, fx_test_args):
+ already_built = set()
+ test_targets = set()
+ manual_tests = set()
+ for name, targets, build in tests:
+ if build_first and build is not None and tuple(
+ build) not in already_built:
+ already_built.add(tuple(build))
+ util.run(build, dry_run, exit_on_failure=True)
+
+ for target in targets:
+ if is_manual_test(target):
+ manual_tests.add(target)
+ else:
+ test_targets.add(target)
+
+ manual_tests = list(manual_tests)
+ test_targets = list(test_targets)
+ if interactive:
+ print('all tests: ')
+ pprint.pprint(manual_tests + test_targets)
+ manual_tests = interactive_filter(manual_tests)
+ test_targets = interactive_filter(test_targets)
+
+ success = True
+ for cmd in manual_tests:
+ success = success and util.run(cmd, dry_run)
+ # print test line that can be copied into a commit message
+ # the absolute FUCHSIA_DIR paths are stripped for readability and
+ # because they are user specific
+ print('Test: ' + cmd.replace(str(util.FUCHSIA_DIR) + '/', ''))
+
+ if test_targets:
+ cmd = ['fx', 'test'] + fx_test_args.split()
+ if not build_first:
+ cmd.append('--no-build')
+ # group all tests into a single `fx test` invocation so that the summary
+ # prints all results
+ cmd.extend(test_targets)
+ success = success and util.run(cmd, dry_run)
+ print('Test: ' + ' '.join(cmd))
+
+ return success
+
+
+def interactive_filter(test_targets):
+ if not test_targets:
+ return []
+ filtered = []
+ for test in test_targets:
+ if input('run {}? (Y/n) '.format(test)) == 'n':
+ continue
+ filtered.append(test)
+ return filtered
+
+
+def is_manual_test(test):
+ """
+ Return whether this is meant to be called with fx test or used as a
+ standalone test command.
+ """
+ # currently fidlc is the only test that doesn't use fx test, since it
+ # uses some fidlc/fidl-compiler-test binary that is not built with the
+ # usual build commands (like fx build zircon/tools, fx ninja -C out/default
+ # host_x64/fidlc)
+ return test == util.TEST_FIDLC
diff --git a/fidldev/test_util.py b/fidldev/test_util.py
new file mode 100644
index 0000000..c70ca7c
--- /dev/null
+++ b/fidldev/test_util.py
@@ -0,0 +1,62 @@
+import argparse
+import contextlib
+import pprint
+
+import env
+
+# mock out these environment consts before importing anything else
+MOCK_FUCHSIA_DIR = 'fuchsia_dir'
+MOCK_BUILD_DIR = 'out/default'
+env.FUCHSIA_DIR = MOCK_FUCHSIA_DIR
+env.BUILD_DIR = MOCK_BUILD_DIR
+env.PLATFORM = 'linux'
+env.MODE = 'clang'
+
+import fidldev
+import util
+
+
+@contextlib.contextmanager
+def mocked_func(func_name, mocked_func):
+ """ Patch util.[func_name] with mocked_func within the specified context. """
+ original = getattr(util, func_name)
+ try:
+ setattr(util, func_name, mocked_func)
+ yield
+ finally:
+ setattr(util, func_name, original)
+
+
+def create_fixed_func(return_values):
+ """ Returns a function that successively returns each of the provided |return_values|. """
+ return_values = iter(return_values)
+
+ def mocked(*args, **kwargs):
+ return next(return_values)
+
+ return mocked
+
+
+def get_commands(mocks, test_cmd):
+ """ Run |test_cmd| with the provided |mocks|, and return the commands that fidldev would have run. """
+ mocked_funcs = [
+ mocked_func(name, create_fixed_func(values))
+ for name, values in mocks.items()
+ ]
+
+ commands = []
+
+ # The arguments and return value of this function need to be kept up to date with util.run
+ def mocked_run(command, dry_run, exit_on_failure=False):
+ commands.append(command)
+ return True
+
+ mocked_funcs.append(mocked_func('run', mocked_run))
+
+ with contextlib.ExitStack() as stack:
+ for func in mocked_funcs:
+ stack.enter_context(func)
+ args = fidldev.parser.parse_args(test_cmd)
+ args.func(args)
+
+ return commands
diff --git a/fidldev/util.py b/fidldev/util.py
new file mode 100644
index 0000000..5baac54
--- /dev/null
+++ b/fidldev/util.py
@@ -0,0 +1,117 @@
+import os
+import subprocess
+
+from env import FUCHSIA_DIR, BUILD_DIR, MODE, PLATFORM
+
+TOPAZ_DIR = os.path.join(FUCHSIA_DIR, 'topaz')
+GO_DIR = os.path.join(FUCHSIA_DIR, 'third_party/go')
+
+FIDLC_DIR = 'zircon/tools/fidl'
+FIDLGEN_DIR = 'garnet/go/src/fidl/compiler'
+FIDLGEN_DART_DIR = 'topaz/bin/fidlgen_dart'
+FIDLGEN_GO_DIR = 'tools/fidl/fidlgen_go'
+FIDLGEN_BACKEND_DIRS = [
+ 'garnet/go/src/fidl/compiler/llcpp_backend',
+ FIDLGEN_GO_DIR,
+ 'tools/fidl/fidlgen_hlcpp',
+ 'tools/fidl/fidlgen_libfuzzer',
+ 'tools/fidl/fidlgen_rust',
+ 'tools/fidl/fidlgen_syzkaller',
+]
+
+TEST_FIDLC = os.path.join(FUCHSIA_DIR, BUILD_DIR, 'host_x64/fidl-compiler')
+FIDLGEN_TEST_TARGETS = [
+ '//garnet/go/src/fidl',
+ '//tools/fidl/fidlgen_hlcpp',
+ '//tools/fidl/fidlgen_go',
+ '//tools/fidl/fidlgen_libfuzzer',
+ '//tools/fidl/fidlgen_rust',
+ '//tools/fidl/fidlgen_syzkaller',
+]
+FIDLGEN_DART_TEST_TARGET = '//topaz/bin/fidlgen_dart'
+HLCPP_TEST_TARGET = '//sdk/lib/fidl'
+LLCPP_TEST_TARGET = '//src/lib/fidl/llcpp'
+C_TEST_TARGET = '//src/lib/fidl/c'
+GO_TEST_TARGET = 'go_fidl_test'
+RUST_TEST_TARGET = '//src/lib/fidl/rust'
+DART_TEST_TARGET = 'fidl_bindings_test'
+GIDL_TEST_TARGET = '//tools/fidl/gidl'
+
+HLCPP_CONFORMANCE_TEST_TARGET = 'conformance_test'
+HLCPP_HOST_CONFORMANCE_TEST_TARGET = 'fidl_cpp_host_conformance_test'
+LLCPP_CONFORMANCE_TEST_TARGET = 'fidl_llcpp_conformance_test'
+GO_CONFORMANCE_TEST_TARGET = 'fidl_go_conformance'
+RUST_CONFORMANCE_TEST_TARGET = 'fidl_conformance_tests'
+
+HLCPP_RUNTIME = 'sdk/lib/fidl'
+LLCPP_RUNTIME = 'src/lib/fidl/llcpp'
+C_RUNTIME = 'zircon/system/ulib/fidl'
+GO_RUNTIME = 'third_party/go/src/syscall/zx/fidl'
+RUST_RUNTIME = 'src/lib/fidl/rust'
+DART_RUNTIME = 'topaz/public/dart/fidl/lib'
+
+BUILD_FIDLC = ['fx', 'build', 'zircon/tools']
+BUILD_FIDLC_TESTS = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidl-compiler']
+BUILD_FIDLGEN = ['fx', 'build', 'garnet/go/src/fidl']
+BUILD_FIDLGEN_DART = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidlgen_dart']
+
+
+def run(command, dry_run, exit_on_failure=False):
+ """
+ Run the given command, returning True if it completed successfuly. If
+ dry_run is true, just prints rather than running. If exit_on_failure is
+ true, exits instead of returning False.
+ """
+ if dry_run:
+ print('would run: {}'.format(command))
+ return True
+ retcode = subprocess.call(command)
+ success = retcode == 0
+ if exit_on_failure and not success:
+ print_err(
+ 'Error: command failed with status {}! {}'.format(retcode, command))
+ exit(1)
+ return success
+
+
+def get_changed_files():
+ """
+ Return a List of paths relative to FUCHSIA_DIR of changed files relative to
+ the parent. This uses the same logic as fx format-code.
+ """
+ upstream = "origin/master"
+ local_commit = subprocess.check_output(
+ "git rev-list HEAD ^{} -- 2>/dev/null | tail -1".format(upstream),
+ shell=True).strip().decode()
+ diff_base = subprocess.check_output(
+ ['git', 'rev-parse', local_commit +
+ '^']).strip().decode() if local_commit else "HEAD"
+ files = subprocess.check_output(['git', 'diff', '--name-only',
+ diff_base]).strip().decode().split('\n')
+
+ repo = subprocess.check_output(['git', 'rev-parse',
+ '--show-toplevel']).strip().decode()
+ # add prefixes so that all and targets can be specified relative to FUCHSIA_DIR
+ if repo.endswith('topaz'):
+ files = [os.path.join('topaz', p) for p in files]
+ elif repo.endswith('third_party/go'):
+ files = [os.path.join('third_party/go', p) for p in files]
+
+ return files
+
+
+RED = '\033[1;31m'
+YELLOW = '\033[1;33m'
+NC = '\033[0m'
+
+
+def print_err(s):
+ print_color(s, RED)
+
+
+def print_warning(s):
+ print_color(s, YELLOW)
+
+
+def print_color(s, color):
+ print('{}{}{}'.format(color, s, NC))