[ffigen] Provide a command to regenerate golden bindings. (#322)

diff --git a/pkgs/ffigen/README.md b/pkgs/ffigen/README.md
index acf3b17..98c7f76 100644
--- a/pkgs/ffigen/README.md
+++ b/pkgs/ffigen/README.md
@@ -509,11 +509,9 @@
 2. Run `dart run ffigen`.
 
 ## Running Tests
-Dynamic library for some tests need to be built before running the examples.
-Run `dart test/setup.dart` to build the libraries.
 
-Run tests from the root of the package with `dart run test`.
-> Note: If llvm is not installed in one of the default locations, tests may fail.
+See [test/README.md](test/README.md)
+
 ## FAQ
 ### Can ffigen be used for removing underscores or renaming declarations?
 Ffigen supports **regexp based renaming**, the regexp must be a
diff --git a/pkgs/ffigen/test/README.md b/pkgs/ffigen/test/README.md
new file mode 100644
index 0000000..1a499a4
--- /dev/null
+++ b/pkgs/ffigen/test/README.md
@@ -0,0 +1,64 @@
+[![Build Status](https://github.com/dart-lang/ffigen/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/ffigen/actions?query=workflow%3A"Dart+CI")
+[![Coverage Status](https://coveralls.io/repos/github/dart-lang/ffigen/badge.svg?branch=master)](https://coveralls.io/github/dart-lang/ffigen?branch=master)
+
+# ffigen testing
+
+## Running Tests
+
+1.  Some tests require that dynamic libraries be built before running the tests.
+    You can do so with:
+
+    ```shell
+    dart run test/setup.dart
+    ```
+
+2.  Run the tests with:
+
+    ```shell
+    dart test
+    ```
+
+## Development
+
+Some tests verify that the generated Dart FFI bindings match a golden file.
+
+For example, the test
+[`test/native_test/native_test.dart`](https://github.com/dart-lang/ffigen/blob/master/test/native_test/native_test.dart)
+works by:
+
+1.  Loading the dynamic library for
+    [`test/native_test/native_test.c`](https://github.com/dart-lang/ffigen/blob/master/test/native_test/native_test.c)
+    (which was generated by `dart run test/setup.dart`).
+
+2.  Generating binding files for that dynamic library in the
+    `test/debug_generated` directory.
+
+3.  Comparing the golden file (i.e.
+    [`test/native_test/native_test_bindings.dart`](https://github.com/dart-lang/ffigen/blob/master/test/native_test/native_test_bindings.dart))
+    to the generated file and generating a test failure if they do not match.
+
+4.  Using the *golden* bindings to excercise the dynamic library.
+
+If you modify any source for a dynamic library (e.g. any `.c`, `.h` or `.m`
+files), then you should run:
+
+```shell
+   dart run test/setup.dart  # Rebuild the dynamic libraries.
+   dart run test/regen.dart  # Rebuild the golden FFI bindings.
+```
+
+If you modify any code that changes how FFI bindings are generated, then you
+should run:
+
+  ```shell
+  dart run test/regen.dart  # Rebuild the golden FFI bindings.
+  ```
+
+A conservative way to run tests is with:
+
+  ```shell
+  dart run test/setup.dart && dart run test/regen.dart && dart test
+  ```
+
+> Note: you should verify that the changes to the golden bindings are
+> reasonable with `git diff`.
diff --git a/pkgs/ffigen/test/regen.dart b/pkgs/ffigen/test/regen.dart
new file mode 100644
index 0000000..1bc2021
--- /dev/null
+++ b/pkgs/ffigen/test/regen.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:ffigen/ffigen.dart';
+import 'package:logging/logging.dart';
+import 'package:yaml/yaml.dart';
+
+const usage = r'''Regenerates the Dart FFI bindings used in tests and examples.
+
+Use this command when developing features that change the generated bindings
+e.g. with this command:
+
+$ dart run test/setup.dart && dart run test/regen.dart && dart test
+''';
+
+Future<void> _regenConfig(File yamlConfig, File bindingOutput,
+    {bool chDir = false}) async {
+  yamlConfig = yamlConfig.absolute;
+  bindingOutput = bindingOutput.absolute;
+
+  Directory? oldDir;
+  var yaml = loadYaml(await yamlConfig.readAsString()) as YamlMap;
+
+  if (chDir) {
+    oldDir = Directory.current;
+    Directory.current = yamlConfig.parent;
+  }
+  try {
+    if (yaml.containsKey("ffigen")) {
+      yaml = yaml["ffigen"] as YamlMap;
+    }
+
+    final config = Config.fromYaml(yaml);
+    final library = parse(config);
+    library.generateFile(bindingOutput);
+  } finally {
+    if (oldDir != null) {
+      Directory.current = oldDir;
+    }
+  }
+}
+
+Future<void> main(List<String> args) async {
+  final parser = ArgParser();
+  parser.addSeparator(usage);
+  parser.addFlag(
+    'help',
+    abbr: 'h',
+    help: 'Prints this usage',
+    negatable: false,
+  );
+
+  final parseArgs = parser.parse(args);
+  if (parseArgs.wasParsed('help')) {
+    print(parser.usage);
+    exit(0);
+  } else if (parseArgs.rest.isNotEmpty) {
+    print(parser.usage);
+    exit(1);
+  }
+
+  Logger.root.level = Level.WARNING;
+  Logger.root.onRecord.listen((record) {
+    print('${record.level.name}: ${record.time}: ${record.message}');
+  });
+
+  await _regenConfig(
+    File('test/native_objc_test/config.yaml'),
+    File('test/native_objc_test/native_objc_test_bindings.dart'),
+  );
+
+  await _regenConfig(File('test/native_test/config.yaml'),
+      File('test/native_test/native_test_bindings.dart'));
+
+  await _regenConfig(File('example/libclang-example/pubspec.yaml'),
+      File('example/libclang-example/generated_bindings.dart'),
+      chDir: true);
+
+  await _regenConfig(File('example/simple/pubspec.yaml'),
+      File('example/simple/generated_bindings.dart'),
+      chDir: true);
+
+  await _regenConfig(File('example/c_json/pubspec.yaml'),
+      File('example/c_json/cjson_generated_bindings.dart'),
+      chDir: true);
+}