| # Compatibility Tests for Fuchsia |
| |
| Compatibility Testing for Fuchsia (CTF) is a mechanism for running |
| different versions of pre-built Fuchsia software against the Fuchsia |
| platform surface area to detect compatibility problems. |
| |
| CTF tests ensure that a client frozen at a previous release milestone |
| is compatible with the Application Programming Interface (API) and |
| Application Binary Interface (ABI) made available to developers via |
| the SDK. It simulates the situation where a behavioral change is |
| landed in the Fuchsia platform that breaks a pre-compiled client. |
| |
| CTF fundamentally consists of the following parts: |
| |
| - A mechanism to freeze artifacts on a release branch and then use |
| those artifacts in a test on the main branch. |
| - A set of build rules to select artifacts to freeze, and a set |
| of build rules to simplify thawing those frozen artifacts on |
| main. |
| - A suite of tests using those build rules to test the compatibility of |
| old client code with the latest platform surface area. |
| - The coverage provided by that suite of tests, in terms of which |
| FIDL methods and syscalls are tested for compatibility across |
| versions. |
| |
| CTF was originally proposed as [RFC 0015][rfc-0015], and the project |
| code is located at [`//sdk/ctf`][sdk-ctf-directory]. |
| |
| ## Motivation |
| |
| The Fuchsia platform defines a surface area consisting of a number of |
| FIDL protocols which are exposed in the Fuchsia SDK. Developers may |
| download and use the Fuchsia SDK to write software targeting Fuchsia systems |
| at a specific *API level*. |
| |
| As the Fuchsia platform surface area evolves over time, changes are |
| represented through availability annotations on FIDL protocols: |
| |
| ```fidl |
| protocol Entry { |
| @available(added=12) |
| SetValue(struct { value string; }); |
| |
| @available(added=20) |
| GetTimestamp() -> (struct { timestamp int64; }); |
| |
| @available(added=13, removed=20) |
| Encrypt(); |
| }; |
| ``` |
| |
| In the above example, the protocol `Entry` has three methods: |
| |
| - `SetValue`, which was added at API level 12 |
| - `GetTimestamp`, which was added at API level 20 |
| - `Encrypt`, which was added at API level 13 but removed at API level |
| 20 when it was found to not be needed any longer. |
| |
| Components built using the Fuchsia SDK must declare the API level |
| they target, affecting which definitions are visible. Consider the following |
| scenarios: |
| |
| - A component is built targeting API level 19: |
| - `SetValue` and `Encrypt` are visible. |
| - `GetTimestamp` is absent, because it did not yet exist at API level 19. |
| - A component is built targeting API level 21: |
| - `SetValue` and `GetTimestamp` are visible. |
| - `Encrypt` is absent, because it was removed at API level 20. |
| |
| Not all API levels are currently supported by the Fuchsia Platform. |
| The canonical list of [supported API levels][version-history] is checked |
| in to the `fuchsia.git` repository and is used to determine which API |
| levels will be supported in releases of the Fuchsia SDK. |
| |
| Targeting a supported API level ensures that a client can connect |
| only to protocols that are supported and guaranteed to be implemented, |
| but it does not provide a guarantee that the behavior of the platform |
| will remain consistent for pre-built components targeting that API |
| level. |
| |
| Ensuring consistent behavior is important to support components |
| built outside of the `fuchsia.git` repository, especially when those |
| components are downloaded as pre-built binaries and assembled into |
| product images. |
| |
| ### Example: A compatibility problem |
| |
| Consider a simplified version of the `Entry` protocol above: |
| |
| ```fidl |
| protocol Entry { |
| @available(added=20) |
| GetTimestamp() -> (struct { timestamp int64; }); |
| }; |
| ``` |
| |
| The `GetTimestamp` method returns the timestamp of an `Entry` *in seconds*. |
| |
| We can test it as follows: |
| |
| ```c++ |
| TEST(Timestamp) { |
| EntrySyncPtr entry = CreateEntryAtMidnightJan1stUTC(); |
| ASSERT_EQ(entry.GetTimestamp(), 1704067200); |
| } |
| ``` |
| |
| Suppose that we decide we actually want nanosecond granularity. We |
| will change the *implementation* of `GetTimestamp` to return |
| nanoseconds, but note that the *FIDL definition* does not change. |
| |
| We would update our test as follows: |
| |
| ```diff |
| TEST(Timestamp) { |
| EntrySyncPtr entry = CreateEntryAtMidnightJan1stUTC(); |
| - ASSERT_EQ(entry.GetTimestamp(), 17040672000); |
| + ASSERT_EQ(entry.GetTimestamp(), 17040672000000000); |
| } |
| ``` |
| |
| This will pass all checks in `fuchsia.git` and submit, but we have now |
| introduced a **compatibility breakage to the Fuchsia Platform**. |
| |
| #### Failure timeline |
| |
| Let's look at a complete timeline that leads to a breakage. |
| |
| 1. **Before the SDK change**: Suppose the FooWidget team wants to |
| implement their FooWidget for Fuchsia. They download the latest |
| Fuchsia SDK and build their component targeting API level 20. This |
| component uses the `Entry` protocol and calls the `GetTimestamp` |
| method. At this point in time, `GetTimestamp` returns seconds. |
| |
| This works for them, so the FooWidget team will publish a Fuchsia |
| package called `foo-widget`. Upon inspection, users of this package |
| see it targets API level 20. |
| |
| 1. **FooWidget is included in a Fuchsia product**: A Fuchsia product |
| is assembled that includes the `foo-widget` package and the Fuchsia |
| platform from an SDK. The `foo-widget` package was ingested as |
| a pre-built binary without source code access, but product assembly |
| sees that the package was built targeting API level 20 and that the |
| Fuchsia platform image supports API level 20. |
| |
| 1. **The SDK change is rolled**: At this point, the change we defined |
| above lands in `fuchsia.git`, a new SDK is produced, and it is |
| released. |
| |
| 1. **The product is assembled with the new SDK**: The product |
| repository rolls to the new SDK and assembles the product using |
| a new Fuchsia platform image combined with the existing pre-built |
| `foo-widget`. |
| |
| 1. **FooWidget is broken on that product**: When the `foo-widget` |
| component calls `GetTimestamp()`, the results will be in nanoseconds |
| rather than seconds, which can have very strange results. |
| For instance, the FooWidget UI may start displaying times millions |
| of years in the future because it is treating nanoseconds as |
| seconds! |
| |
| It is dangerous to change the behavior of platform functionality |
| when pre-existing software depends on the old behavior. |
| |
| ### Catching compatibility problems |
| |
| CTF tests simulate the above scenario by taking advantage of Fuchsia's |
| release branching process. If a CTF test fails, it means that |
| pre-built components targeting an old Fuchsia platform implementation |
| will fail when targeting the latest platform implementation. It works |
| as follows: |
| |
| 1. Each Fuchsia [milestone release][milestone-release] has an |
| associated release branch in `fuchsia.git`. This represents the |
| state of Fuchsia's platform implementation at the time of release. |
| 1. That platform implementation passes the set of tests on that |
| release branch. |
| 1. A set of artifacts are included in a *CTF Artifacts* bundle for |
| that release branch. |
| 1. When the release branch is created and/or modified, the CTF |
| Artifacts for that branch are compiled and uploaded to CIPD. |
| 1. The CTF Artifacts for each supported API level are downloaded as |
| pre-builts on the `main` branch and are *thawed* by turning them |
| into test packages. |
| 1. Those test packages are run against the latest Fuchsia platform |
| on the `main` branch, which will block CL submission if they fail. |
| |
| This exactly maps to the scenario in which a component is built |
| against an old Fuchsia SDK and platform, and then is run as a |
| pre-built against the current Fuchsia platform. |
| |
| ## Background and history |
| |
| CTF exists to prevent breaking changes to the implemented Fuchsia |
| API and ABI from landing in the platform. |
| |
| CTF originally was designed to provide confidence that an arbitrary |
| Fuchsia system correctly implemented the platform surface area for |
| a particular ABI revision. This would mean that components built |
| for that revision would work on the system, and that the system is |
| backwards compatible with that revision. |
| |
| The current purpose of CTF is to provide a mechanism for freezing |
| and "thawing" test artifacts across release branches. |
| |
| This mechanism can be applied to ensure the current Fuchsia platform |
| supports the old API and ABI behavior for each version that is |
| supposed to be supported. Additionally, the frozen tests may be |
| applied to arbitrary Fuchsia images to achieve the original goal |
| of CTF if desired. |
| |
| <!-- Reference links --> |
| |
| [rfc-0015]: /docs/contribute/governance/rfcs/0015_cts.md |
| [sdk-ctf-directory]: https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/sdk/ctf/ |
| [version-history]: https://fuchsia.googlesource.com/fuchsia/+/HEAD/sdk/version_history.json |
| [milestone-release]: /docs/concepts/versioning/release.md#milestone |