assemble_vintf: build and fuse compatibility matrix at build time.

The fused matrix:
(1) has all requirements in compatibilility_matrix.V.xml, where V
is the "level" attribute in device manifest (or is inferred if missing)
(2) has all other HALs as optional HALs in compatibility_matrix.U.xml
files, where U > V, including current.xml.

Bug: 69636193

Test: m (system_)compatibility_matrix/manifest.xml
Test: libvintf_test

Change-Id: I01d2f1ea779e270a9b3ca8de63a064747f36eb2d
Merged-In: I01d2f1ea779e270a9b3ca8de63a064747f36eb2d
diff --git a/CompatibilityMatrix.cpp b/CompatibilityMatrix.cpp
index f788569..521c44b 100644
--- a/CompatibilityMatrix.cpp
+++ b/CompatibilityMatrix.cpp
@@ -16,6 +16,8 @@
 
 #include "CompatibilityMatrix.h"
 
+#include <utility>
+
 #include "parse_string.h"
 #include "utils.h"
 
@@ -71,6 +73,63 @@
     return "";
 }
 
+static VersionRange* findRangeWithMajorVersion(std::vector<VersionRange>& versionRanges,
+                                               size_t majorVer) {
+    for (VersionRange& vr : versionRanges) {
+        if (vr.majorVer == majorVer) {
+            return &vr;
+        }
+    }
+    return nullptr;
+}
+
+std::pair<MatrixHal*, VersionRange*> CompatibilityMatrix::getHalWithMajorVersion(
+    const std::string& name, size_t majorVer) {
+    for (MatrixHal* hal : getHals(name)) {
+        VersionRange* vr = findRangeWithMajorVersion(hal->versionRanges, majorVer);
+        if (vr != nullptr) {
+            return {hal, vr};
+        }
+    }
+    return {nullptr, nullptr};
+}
+
+bool CompatibilityMatrix::addAllHalsAsOptional(CompatibilityMatrix* other, std::string* error) {
+    if (other == nullptr || other->level() <= level()) {
+        return true;
+    }
+
+    for (auto& pair : other->mHals) {
+        const std::string& name = pair.first;
+        MatrixHal& halToAdd = pair.second;
+        for (const VersionRange& vr : halToAdd.versionRanges) {
+            MatrixHal* existingHal;
+            VersionRange* existingVr;
+            std::tie(existingHal, existingVr) = getHalWithMajorVersion(name, vr.majorVer);
+
+            if (existingHal == nullptr) {
+                halToAdd.optional = true;
+                add(std::move(halToAdd));
+                continue;
+            }
+
+            if (!existingHal->optional && !existingHal->containsInstances(halToAdd)) {
+                if (error != nullptr) {
+                    *error = "HAL " + name + "@" + to_string(vr.minVer()) + " is a required " +
+                             "HAL, but fully qualified instance names don't match (at FCM "
+                             "Version " +
+                             std::to_string(level()) + " and " + std::to_string(other->level()) +
+                             ")";
+                }
+                return false;
+            }
+
+            existingVr->maxMinor = std::max(existingVr->maxMinor, vr.maxMinor);
+        }
+    }
+    return true;
+}
+
 bool operator==(const CompatibilityMatrix &lft, const CompatibilityMatrix &rgt) {
     return lft.mType == rgt.mType && lft.mLevel == rgt.mLevel && lft.mHals == rgt.mHals &&
            lft.mXmlFiles == rgt.mXmlFiles &&
diff --git a/MatrixHal.cpp b/MatrixHal.cpp
index 50c5116..96dcf78 100644
--- a/MatrixHal.cpp
+++ b/MatrixHal.cpp
@@ -48,5 +48,23 @@
     return ret;
 }
 
+bool MatrixHal::containsInstances(const MatrixHal& other) const {
+    for (const auto& pair : other.interfaces) {
+        const std::string& interfaceName = pair.first;
+        auto thisIt = interfaces.find(interfaceName);
+        if (thisIt == interfaces.end()) {
+            return false;
+        }
+
+        const std::set<std::string>& thisInstances = thisIt->second.instances;
+        const std::set<std::string>& otherInstances = pair.second.instances;
+        if (!std::includes(thisInstances.begin(), thisInstances.end(), otherInstances.begin(),
+                           otherInstances.end())) {
+            return false;
+        }
+    }
+    return true;
+}
+
 } // namespace vintf
 } // namespace android
diff --git a/assemble_vintf.cpp b/assemble_vintf.cpp
index d031d83..19294c6 100644
--- a/assemble_vintf.cpp
+++ b/assemble_vintf.cpp
@@ -25,6 +25,7 @@
 #include <string>
 
 #include <android-base/file.h>
+#include <android-base/parseint.h>
 
 #include <vintf/KernelConfigParser.h>
 #include <vintf/parse_string.h>
@@ -63,6 +64,24 @@
         return true;
     }
 
+    static bool getBooleanFlag(const char* key) {
+        const char* envValue = getenv(key);
+        return envValue != nullptr && strcmp(envValue, "true") == 0;
+    }
+
+    static size_t getIntegerFlag(const char* key, size_t defaultValue = 0) {
+        std::string envValue = getenv(key);
+        if (envValue.empty()) {
+            return defaultValue;
+        }
+        size_t value;
+        if (!base::ParseUint(envValue, &value)) {
+            std::cerr << "Error: " << key << " must be a number." << std::endl;
+            return defaultValue;
+        }
+        return value;
+    }
+
     static std::string read(std::basic_istream<char>& is) {
         std::stringstream ss;
         ss << is.rdbuf();
@@ -73,6 +92,18 @@
         return ::android::base::Basename(path) == gBaseConfig;
     }
 
+    static Level convertFromApiLevel(size_t apiLevel) {
+        if (apiLevel < 26) {
+            return Level::LEGACY;
+        } else if (apiLevel == 26) {
+            return Level::O;
+        } else if (apiLevel == 27) {
+            return Level::O_MR1;
+        } else {
+            return Level::UNSPECIFIED;
+        }
+    }
+
     // nullptr on any error, otherwise the condition.
     static Condition generateCondition(const std::string& path) {
         std::string fname = ::android::base::Basename(path);
@@ -173,17 +204,59 @@
         return ret;
     }
 
+    static std::string getFileNameFromPath(std::string path) {
+        auto idx = path.find_last_of("\\/");
+        if (idx != std::string::npos) {
+            path.erase(0, idx + 1);
+        }
+        return path;
+    }
+
     std::basic_ostream<char>& out() const {
         return mOutFileRef == nullptr ? std::cout : *mOutFileRef;
     }
 
-    bool assembleHalManifest(HalManifest* halManifest) {
+    template <typename S>
+    using Schemas = std::vector<std::pair<std::string, S>>;
+    using HalManifests = Schemas<HalManifest>;
+    using CompatibilityMatrices = Schemas<CompatibilityMatrix>;
+
+    bool assembleHalManifest(HalManifests* halManifests) {
         std::string error;
+        HalManifest* halManifest = &halManifests->front().second;
+        for (auto it = halManifests->begin() + 1; it != halManifests->end(); ++it) {
+            const std::string& path = it->first;
+            HalManifest& halToAdd = it->second;
+
+            if (halToAdd.level() != Level::UNSPECIFIED) {
+                if (halManifest->level() == Level::UNSPECIFIED) {
+                    halManifest->mLevel = halToAdd.level();
+                } else if (halManifest->level() != halToAdd.level()) {
+                    std::cerr << "Inconsistent FCM Version in HAL manifests:" << std::endl
+                              << "    File '" << halManifests->front().first << "' has level "
+                              << halManifest->level() << std::endl
+                              << "    File '" << path << "' has level " << halToAdd.level()
+                              << std::endl;
+                    return false;
+                }
+            }
+
+            if (!halManifest->addAll(std::move(halToAdd), &error)) {
+                std::cerr << "File \"" << path << "\" cannot be added: conflict on HAL \"" << error
+                          << "\" with an existing HAL. See <hal> with the same name "
+                          << "in previously parsed files or previously declared in this file."
+                          << std::endl;
+                return false;
+            }
+        }
 
         if (halManifest->mType == SchemaType::DEVICE) {
             if (!getFlag("BOARD_SEPOLICY_VERS", &halManifest->device.mSepolicyVersion)) {
                 return false;
             }
+            if (!setDeviceFcmVersion(halManifest)) {
+                return false;
+            }
         }
 
         if (mOutputMatrix) {
@@ -248,12 +321,107 @@
         return true;
     }
 
-    bool assembleCompatibilityMatrix(CompatibilityMatrix* matrix) {
-        std::string error;
+    bool setDeviceFcmVersion(HalManifest* manifest) {
+        size_t shippingApiLevel = getIntegerFlag("PRODUCT_SHIPPING_API_LEVEL");
 
+        if (manifest->level() != Level::UNSPECIFIED) {
+            return true;
+        }
+        if (!getBooleanFlag("PRODUCT_ENFORCE_VINTF_MANIFEST")) {
+            manifest->mLevel = Level::LEGACY;
+            return true;
+        }
+        // TODO(b/70628538): Do not infer from Shipping API level.
+        if (shippingApiLevel) {
+            std::cerr << "Warning: Shipping FCM Version is inferred from Shipping API level. "
+                      << "Declare Shipping FCM Version in device manifest directly." << std::endl;
+            manifest->mLevel = convertFromApiLevel(shippingApiLevel);
+            if (manifest->mLevel == Level::UNSPECIFIED) {
+                std::cerr << "Error: Shipping FCM Version cannot be inferred from Shipping API "
+                          << "level " << shippingApiLevel << "."
+                          << "Declare Shipping FCM Version in device manifest directly."
+                          << std::endl;
+                return false;
+            }
+            return true;
+        }
+        // TODO(b/69638851): should be an error if Shipping API level is not defined.
+        // For now, just leave it empty; when framework compatibility matrix is built,
+        // lowest FCM Version is assumed.
+        std::cerr << "Warning: Shipping FCM Version cannot be inferred, because:" << std::endl
+                  << "    (1) It is not explicitly declared in device manifest;" << std::endl
+                  << "    (2) PRODUCT_ENFORCE_VINTF_MANIFEST is set to true;" << std::endl
+                  << "    (3) PRODUCT_SHIPPING_API_LEVEL is undefined." << std::endl
+                  << "Assuming 'unspecified' Shipping FCM Version. " << std::endl
+                  << "To remove this warning, define 'level' attribute in device manifest."
+                  << std::endl;
+        return true;
+    }
+
+    Level getLowestFcmVersion(const CompatibilityMatrices& matrices) {
+        Level ret = Level::UNSPECIFIED;
+        for (const auto& e : matrices) {
+            if (ret == Level::UNSPECIFIED || ret > e.second.level()) {
+                ret = e.second.level();
+            }
+        }
+        return ret;
+    }
+
+    bool assembleCompatibilityMatrix(CompatibilityMatrices* matrices) {
+        std::string error;
+        CompatibilityMatrix* matrix = nullptr;
         KernelSepolicyVersion kernelSepolicyVers;
         Version sepolicyVers;
-        if (matrix->mType == SchemaType::FRAMEWORK) {
+        std::unique_ptr<HalManifest> checkManifest;
+        if (matrices->front().second.mType == SchemaType::DEVICE) {
+            matrix = &matrices->front().second;
+        }
+
+        if (matrices->front().second.mType == SchemaType::FRAMEWORK) {
+            Level deviceLevel = Level::UNSPECIFIED;
+            std::vector<std::string> fileList;
+            if (mCheckFile.is_open()) {
+                checkManifest = std::make_unique<HalManifest>();
+                if (!gHalManifestConverter(checkManifest.get(), read(mCheckFile))) {
+                    std::cerr << "Cannot parse check file as a HAL manifest: "
+                              << gHalManifestConverter.lastError() << std::endl;
+                    return false;
+                }
+                deviceLevel = checkManifest->level();
+            }
+
+            if (deviceLevel == Level::UNSPECIFIED) {
+                // For GSI build, legacy devices that do not have a HAL manifest,
+                // and devices in development, merge all compatibility matrices.
+                deviceLevel = getLowestFcmVersion(*matrices);
+            }
+
+            for (auto& e : *matrices) {
+                if (e.second.level() == deviceLevel) {
+                    fileList.push_back(e.first);
+                    matrix = &e.second;
+                }
+            }
+            if (matrix == nullptr) {
+                std::cerr << "FATAL ERROR: cannot find matrix with level '" << deviceLevel << "'"
+                          << std::endl;
+                return false;
+            }
+            for (auto& e : *matrices) {
+                if (e.second.level() <= deviceLevel) {
+                    continue;
+                }
+                fileList.push_back(e.first);
+                if (!matrix->addAllHalsAsOptional(&e.second, &error)) {
+                    std::cerr << "File \"" << e.first << "\" cannot be added: " << error
+                              << ". See <hal> with the same name "
+                              << "in previously parsed files or previously declared in this file."
+                              << std::endl;
+                    return false;
+                }
+            }
+
             if (!getFlag("BOARD_SEPOLICY_VERS", &sepolicyVers)) {
                 return false;
             }
@@ -273,21 +441,21 @@
                 return false;
             }
             matrix->framework.mAvbMetaVersion = avbMetaVersion;
+
+            out() << "<!--" << std::endl;
+            out() << "    Input:" << std::endl;
+            for (const auto& path : fileList) {
+                out() << "        " << getFileNameFromPath(path) << std::endl;
+            }
+            out() << "-->" << std::endl;
         }
         out() << gCompatibilityMatrixConverter(*matrix, mSerializeFlags);
         out().flush();
 
-        if (mCheckFile.is_open()) {
-            HalManifest checkManifest;
-            if (!gHalManifestConverter(&checkManifest, read(mCheckFile))) {
-                std::cerr << "Cannot parse check file as a HAL manifest: "
-                          << gHalManifestConverter.lastError() << std::endl;
-                return false;
-            }
-            if (!checkManifest.checkCompatibility(*matrix, &error)) {
-                std::cerr << "Not compatible: " << error << std::endl;
-                return false;
-            }
+        if (checkManifest != nullptr && getBooleanFlag("PRODUCT_ENFORCE_VINTF_MANIFEST") &&
+            !checkManifest->checkCompatibility(*matrix, &error)) {
+            std::cerr << "Not compatible: " << error << std::endl;
+            return false;
         }
 
         return true;
@@ -297,38 +465,33 @@
     template <typename Schema, typename AssembleFunc>
     AssembleStatus tryAssemble(const XmlConverter<Schema>& converter, const std::string& schemaName,
                                AssembleFunc assemble) {
+        Schemas<Schema> schemas;
         Schema schema;
         if (!converter(&schema, read(mInFiles.front()))) {
             return TRY_NEXT;
         }
         auto firstType = schema.type();
+        schemas.emplace_back(mInFilePaths.front(), std::move(schema));
+
         for (auto it = mInFiles.begin() + 1; it != mInFiles.end(); ++it) {
             Schema additionalSchema;
+            const std::string fileName = mInFilePaths[std::distance(mInFiles.begin(), it)];
             if (!converter(&additionalSchema, read(*it))) {
-                std::cerr << "File \"" << mInFilePaths[std::distance(mInFiles.begin(), it)]
-                          << "\" is not a valid " << firstType << " " << schemaName
-                          << " (but the first file is a valid " << firstType << " " << schemaName
-                          << "). Error: " << converter.lastError() << std::endl;
+                std::cerr << "File \"" << fileName << "\" is not a valid " << firstType << " "
+                          << schemaName << " (but the first file is a valid " << firstType << " "
+                          << schemaName << "). Error: " << converter.lastError() << std::endl;
                 return FAIL_AND_EXIT;
             }
             if (additionalSchema.type() != firstType) {
-                std::cerr << "File \"" << mInFilePaths[std::distance(mInFiles.begin(), it)]
-                          << "\" is a " << additionalSchema.type() << " " << schemaName
-                          << " (but a " << firstType << " " << schemaName << " is expected)."
-                          << std::endl;
+                std::cerr << "File \"" << fileName << "\" is a " << additionalSchema.type() << " "
+                          << schemaName << " (but a " << firstType << " " << schemaName
+                          << " is expected)." << std::endl;
                 return FAIL_AND_EXIT;
             }
-            std::string error;
-            if (!schema.addAll(std::move(additionalSchema), &error)) {
-                std::cerr << "File \"" << mInFilePaths[std::distance(mInFiles.begin(), it)]
-                          << "\" cannot be added: conflict on HAL \"" << error
-                          << "\" with an existing HAL. See <hal> with the same name "
-                          << "in previously parsed files or previously declared in this file."
-                          << std::endl;
-                return FAIL_AND_EXIT;
-            }
+
+            schemas.emplace_back(fileName, std::move(additionalSchema));
         }
-        return assemble(&schema) ? SUCCESS : FAIL_AND_EXIT;
+        return assemble(&schemas) ? SUCCESS : FAIL_AND_EXIT;
     }
 
     bool assemble() {
diff --git a/include/vintf/CompatibilityMatrix.h b/include/vintf/CompatibilityMatrix.h
index c9a8a9f..23cbe53 100644
--- a/include/vintf/CompatibilityMatrix.h
+++ b/include/vintf/CompatibilityMatrix.h
@@ -59,6 +59,16 @@
     bool add(MatrixHal &&hal);
     bool add(MatrixKernel &&kernel);
 
+    // Add all HALs as optional HALs from "other". This function moves MatrixHal objects
+    // from "other".
+    // Require other->level() > this->level(), otherwise do nothing.
+    bool addAllHalsAsOptional(CompatibilityMatrix* other, std::string* error);
+    // Return the MatrixHal object with the given name and major version. Since all major
+    // version are guaranteed distinct when add()-ed, there should be at most 1 match.
+    // Return nullptr if none is found.
+    std::pair<MatrixHal*, VersionRange*> getHalWithMajorVersion(const std::string& name,
+                                                                size_t majorVer);
+
     status_t fetchAllInformation(const std::string &path);
 
     friend struct HalManifest;
diff --git a/include/vintf/MatrixHal.h b/include/vintf/MatrixHal.h
index 203aa57..47094fb 100644
--- a/include/vintf/MatrixHal.h
+++ b/include/vintf/MatrixHal.h
@@ -48,6 +48,9 @@
     inline bool hasInterface(const std::string& interface_name) const {
         return interfaces.find(interface_name) != interfaces.end();
     }
+
+    // Return true if "this" contains all interface/instance instances in "other".
+    bool containsInstances(const MatrixHal& other) const;
 };
 
 } // namespace vintf
diff --git a/test/main.cpp b/test/main.cpp
index b8a25a2..09f2ad0 100644
--- a/test/main.cpp
+++ b/test/main.cpp
@@ -103,6 +103,9 @@
         return mh.isValid();
     }
     std::vector<MatrixKernel>& getKernels(CompatibilityMatrix& cm) { return cm.framework.mKernels; }
+    bool addAllHalsAsOptional(CompatibilityMatrix* cm1, CompatibilityMatrix* cm2, std::string* e) {
+        return cm1->addAllHalsAsOptional(cm2, e);
+    }
 
     std::map<std::string, HalInterface> testHalInterfaces() {
         HalInterface intf;
@@ -2084,6 +2087,151 @@
     EXPECT_EQ(1u, manifest.level());
 }
 
+TEST_F(LibVintfTest, AddOptionalHal) {
+    CompatibilityMatrix cm1;
+    CompatibilityMatrix cm2;
+    std::string error;
+    std::string xml;
+
+    xml = "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\"/>";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm1, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    xml =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"2\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0-1</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm2, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    EXPECT_TRUE(addAllHalsAsOptional(&cm1, &cm2, &error)) << error;
+    xml = gCompatibilityMatrixConverter(cm1, SerializeFlag::HALS_ONLY);
+    EXPECT_EQ(xml,
+              "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+              "    <hal format=\"hidl\" optional=\"true\">\n"
+              "        <name>android.hardware.foo</name>\n"
+              "        <version>1.0-1</version>\n"
+              "        <interface>\n"
+              "            <name>IFoo</name>\n"
+              "            <instance>default</instance>\n"
+              "        </interface>\n"
+              "    </hal>\n"
+              "</compatibility-matrix>\n");
+}
+
+TEST_F(LibVintfTest, AddOptionalHalMinorVersion) {
+    CompatibilityMatrix cm1;
+    CompatibilityMatrix cm2;
+    std::string error;
+    std::string xml;
+
+    xml =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.2-3</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm1, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    xml =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"2\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.0-4</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm2, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    EXPECT_TRUE(addAllHalsAsOptional(&cm1, &cm2, &error)) << error;
+    xml = gCompatibilityMatrixConverter(cm1, SerializeFlag::HALS_ONLY);
+    EXPECT_EQ(xml,
+              "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+              "    <hal format=\"hidl\" optional=\"false\">\n"
+              "        <name>android.hardware.foo</name>\n"
+              "        <version>1.2-4</version>\n"
+              "        <interface>\n"
+              "            <name>IFoo</name>\n"
+              "            <instance>default</instance>\n"
+              "        </interface>\n"
+              "    </hal>\n"
+              "</compatibility-matrix>\n");
+}
+
+TEST_F(LibVintfTest, AddOptionalHalMajorVersion) {
+    CompatibilityMatrix cm1;
+    CompatibilityMatrix cm2;
+    std::string error;
+    std::string xml;
+
+    xml =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>1.2-3</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm1, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    xml =
+        "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"2\">\n"
+        "    <hal format=\"hidl\" optional=\"false\">\n"
+        "        <name>android.hardware.foo</name>\n"
+        "        <version>2.0-4</version>\n"
+        "        <interface>\n"
+        "            <name>IFoo</name>\n"
+        "            <instance>default</instance>\n"
+        "        </interface>\n"
+        "    </hal>\n"
+        "</compatibility-matrix>\n";
+    EXPECT_TRUE(gCompatibilityMatrixConverter(&cm2, xml))
+        << gCompatibilityMatrixConverter.lastError();
+
+    EXPECT_TRUE(addAllHalsAsOptional(&cm1, &cm2, &error)) << error;
+    xml = gCompatibilityMatrixConverter(cm1, SerializeFlag::HALS_ONLY);
+    EXPECT_EQ(xml,
+              "<compatibility-matrix version=\"1.0\" type=\"framework\" level=\"1\">\n"
+              "    <hal format=\"hidl\" optional=\"false\">\n"
+              "        <name>android.hardware.foo</name>\n"
+              "        <version>1.2-3</version>\n"
+              "        <interface>\n"
+              "            <name>IFoo</name>\n"
+              "            <instance>default</instance>\n"
+              "        </interface>\n"
+              "    </hal>\n"
+              "    <hal format=\"hidl\" optional=\"true\">\n"
+              "        <name>android.hardware.foo</name>\n"
+              "        <version>2.0-4</version>\n"
+              "        <interface>\n"
+              "            <name>IFoo</name>\n"
+              "            <instance>default</instance>\n"
+              "        </interface>\n"
+              "    </hal>\n"
+              "</compatibility-matrix>\n");
+}
+
 } // namespace vintf
 } // namespace android