From db607a379b00f8c8cb6f17c4ce4dc4f43d78af36 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Wed, 23 Sep 2026 10:36:07 +0200 Subject: [PATCH 1/2] test(svdconv): expand unit and generator coverage Add focused model, conversion, parser and output tests without changing production code or coverage thresholds. --- tools/svdconv/test/integtests/CMakeLists.txt | 3 +- .../integtests/src/SvdConvEnumHeaderTests.cpp | 83 ++++ .../test/integtests/src/SvdConvIntegTests.cpp | 81 ++++ tools/svdconv/test/unittests/CMakeLists.txt | 11 +- .../src/SvdCExpressionParserTest.cpp | 84 ++++ .../test/unittests/src/SvdCpuModelTest.cpp | 382 +++++++++++++++ .../unittests/src/SvdModelValidationTest.cpp | 292 +++++++++++ .../test/unittests/src/SvdOptionsTest.cpp | 220 +++++++++ .../unittests/src/SvdUtilsConversionTest.cpp | 456 ++++++++++++++++++ 9 files changed, 1610 insertions(+), 2 deletions(-) create mode 100644 tools/svdconv/test/integtests/src/SvdConvEnumHeaderTests.cpp create mode 100644 tools/svdconv/test/unittests/src/SvdCExpressionParserTest.cpp create mode 100644 tools/svdconv/test/unittests/src/SvdCpuModelTest.cpp create mode 100644 tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp create mode 100644 tools/svdconv/test/unittests/src/SvdOptionsTest.cpp create mode 100644 tools/svdconv/test/unittests/src/SvdUtilsConversionTest.cpp diff --git a/tools/svdconv/test/integtests/CMakeLists.txt b/tools/svdconv/test/integtests/CMakeLists.txt index 89e9bcc30..422e009ce 100644 --- a/tools/svdconv/test/integtests/CMakeLists.txt +++ b/tools/svdconv/test/integtests/CMakeLists.txt @@ -1,4 +1,5 @@ -SET(SOURCE_FILES SvdConvIntegTestEnv.cpp SvdConvIntegTests.cpp SvdConvTestUtils.cpp SvdConvTestCmdLine.cpp) +SET(SOURCE_FILES SvdConvIntegTestEnv.cpp SvdConvIntegTests.cpp SvdConvTestUtils.cpp SvdConvTestCmdLine.cpp + SvdConvEnumHeaderTests.cpp) SET(HEADER_FILES SvdConvIntegTestEnv.h SvdConvTestUtils.h) list(TRANSFORM SOURCE_FILES PREPEND src/) diff --git a/tools/svdconv/test/integtests/src/SvdConvEnumHeaderTests.cpp b/tools/svdconv/test/integtests/src/SvdConvEnumHeaderTests.cpp new file mode 100644 index 000000000..08941432e --- /dev/null +++ b/tools/svdconv/test/integtests/src/SvdConvEnumHeaderTests.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "SvdConvIntegTestEnv.h" +#include "SvdConvTestUtils.h" +#include "SVDConv.h" +#include "ErrLog.h" + +#include +#include +#include +#include +#include + +using namespace std; + +namespace { + +class SvdConvEnumHeaderTests : public testing::Test { +protected: + void SetUp() override { + ErrLog::Get()->ClearLogMessages(); + } + + void TearDown() override { + ErrLog::Get()->Save(); + ErrLog::Get()->ClearLogMessages(); + } + + void GenerateHeader(const string& fixture, const string& name, string& header) { + const string input = SvdConvIntegTestEnv::localtestdata_dir + "/" + fixture; + const string output = SvdConvIntegTestEnv::testoutput_dir + "/enumHeader/" + name; + ASSERT_TRUE(RteFsUtils::Exists(input)); + Arguments args("SVDConv.exe", input); + args.add({"-o", output, "--generate=header", "--fields=enum", "--create-folder"}); + SvdConv svdConv; + ASSERT_EQ(0, svdConv.Check(args, args, nullptr)); + + for(const auto& message : ErrLog::Get()->GetLogMessages()) { + EXPECT_EQ(string::npos, message.find("M227")) << message; + } + ASSERT_TRUE(RteFsUtils::ReadFile(output + "/" + name + ".h", header)); + ASSERT_FALSE(header.empty()); + } + + static void ExpectEnumeration(const string& header, const string& type, + initializer_list> values) { + const regex declaration(R"(typedef\s+enum\s*\{([^}]+)\}\s*)" + type + R"(\s*;)"); + const auto types = SvdConvTestUtils::FindRegex(header, declaration); + ASSERT_EQ(1U, types.size()) << type; + const string body = types.front()[1].str(); + for(const auto& [name, expected] : values) { + SCOPED_TRACE(name); + const regex assignment(string(R"(\b)") + name + R"(\s*=\s*(0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*\s*,)"); + const auto entries = SvdConvTestUtils::FindRegex(body, assignment); + ASSERT_EQ(1U, entries.size()); + EXPECT_EQ(expected, stoul(entries.front()[1].str(), nullptr, 0)); + } + } +}; + +TEST_F(SvdConvEnumHeaderTests, FieldEnumsPreserveValuesWithoutBitOffsetShift) { + string header; + ASSERT_NO_FATAL_FAILURE(GenerateHeader("enumComboWidth/EnumComboWidth.svd", "EnumComboWidth", header)); + + ExpectEnumeration(header, "TEST_CTRL_SIX_BIT_Enum", {{"TEST_CTRL_SIX_BIT_ONE", 1U}}); + // SEVEN_BIT starts at bit 8, but its enum value is the field value, not a register mask. + ExpectEnumeration(header, "TEST_CTRL_SEVEN_BIT_Enum", {{"TEST_CTRL_SEVEN_BIT_NINE", 9U}}); +} + +TEST_F(SvdConvEnumHeaderTests, ClusterAndDerivedRegisterEnumsHaveDistinctTypesAndValues) { + string header; + ASSERT_NO_FATAL_FAILURE(GenerateHeader("option_n/option_n.svd", "option_n", header)); + + ExpectEnumeration(header, "DCB_DSCSR_Clust_DSCSR_CDS_Enum", + {{"DCB_DSCSR_Clust_DSCSR_CDS_Disable", 0U}, {"DCB_DSCSR_Clust_DSCSR_CDS_Enable", 1U}}); + ExpectEnumeration(header, "DCB_DSCSR_CDS_Enum", + {{"DCB_DSCSR_CDS_Disable", 0U}, {"DCB_DSCSR_CDS_Enable", 1U}}); +} + +} // namespace diff --git a/tools/svdconv/test/integtests/src/SvdConvIntegTests.cpp b/tools/svdconv/test/integtests/src/SvdConvIntegTests.cpp index 319302124..2415549f4 100644 --- a/tools/svdconv/test/integtests/src/SvdConvIntegTests.cpp +++ b/tools/svdconv/test/integtests/src/SvdConvIntegTests.cpp @@ -131,6 +131,87 @@ TEST_F(SvdConvIntegTests, CheckSfdGeneration) { EXPECT_FALSE(sfd.empty()); } +TEST_F(SvdConvIntegTests, GeneratesMemoryMapsAtRequestedDetailLevel) { + const string inFile = SvdConvIntegTestEnv::localtestdata_dir + "/option_n/option_n.svd"; + const string testOut = SvdConvIntegTestEnv::testoutput_dir + "/memoryMaps"; + ASSERT_TRUE(RteFsUtils::Exists(inFile)); + + struct MapMode { + const char* option; + const char* suffix; + bool registers; + bool fields; + }; + const MapMode modes[] = { + {"peripheralMap", "MapPeripherals", false, false}, + {"registerMap", "MapRegisters", true, false}, + {"fieldMap", "MapFields", true, true}, + }; + for(const auto& mode : modes) { + SCOPED_TRACE(mode.option); + Arguments args("SVDConv.exe", inFile); + args.add({"-o", testOut, string("--generate=") + mode.option, "--create-folder"}); + SvdConv svdConv; + ASSERT_EQ(0, svdConv.Check(args, args, nullptr)); + + string listing; + ASSERT_TRUE(RteFsUtils::ReadFile(testOut + "/option_n_" + mode.suffix + ".txt", listing)); + EXPECT_NE(string::npos, listing.find("Peripheral Map")); + for(const auto* address : {"0x40003000", "0x40004000", "0xe000ee08"}) { + EXPECT_NE(string::npos, listing.find(string("Base Address: ") + address)); + } + EXPECT_NE(string::npos, listing.find("AddressBlock:")); + EXPECT_TRUE(regex_search(listing, regex(R"(000\s+---)"))); + EXPECT_TRUE(regex_search(listing, regex(R"(001\s+---)"))); + EXPECT_TRUE(regex_search(listing, regex(R"(002\s+IWDG\s+IWDG Interrupt)"))); + + // Derived peripherals and nested clusters must retain their resolved addresses. + for(const auto* pattern : { + R"(\bKR\b[^\n]*Address: 0x40003000,[^\n]*Access: wo)", + R"(\bKR\b[^\n]*Address: 0x40004000,[^\n]*Access: wo)", + R"(\bPR\b[^\n]*Address: 0x40003004,[^\n]*Offset: 0x00000004,[^\n]*Width: 4,[^\n]*Access: rw)", + R"(\bDSCSR_Clust\b[^\n]*Address: 0xe000ee08)", + R"(\bDSCSR\b[^\n]*Address: 0xe000ee08)", + R"(\bDSCSR\b[^\n]*Address: 0xe000ee10)"}) { + EXPECT_EQ(mode.registers, regex_search(listing, regex(pattern))) << pattern; + } + for(const auto* pattern : { + R"(\bKEY\b[^\n]*\[15 \.\.\. 0\][^\n]*Bits: 16)", + R"(\bCDS\b[^\n]*\[16 \.\.\. 16\][^\n]*Bits: 1)"}) { + EXPECT_EQ(mode.fields, regex_search(listing, regex(pattern))) << pattern; + } + } +} + +TEST_F(SvdConvIntegTests, ExpandsRegisterAndFieldDimensionsInMemoryMap) { + const string inFile = SvdConvIntegTestEnv::localtestdata_dir + "/posMaskDim/PosMaskDim.svd"; + const string testOut = SvdConvIntegTestEnv::testoutput_dir + "/dimensionMemoryMap"; + ASSERT_TRUE(RteFsUtils::Exists(inFile)); + Arguments args("SVDConv.exe", inFile); + args.add({"-o", testOut, "--generate=fieldMap", "--create-folder"}); + SvdConv svdConv; + ASSERT_EQ(0, svdConv.Check(args, args, nullptr)); + + string listing; + ASSERT_TRUE(RteFsUtils::ReadFile(testOut + "/PosMaskDim_MapFields.txt", listing)); + const regex registers(R"(\bDATA(\d+)\b[^\n]*Address: 0x([0-9a-f]+))"); + unsigned count = 0; + for(auto it = sregex_iterator(listing.begin(), listing.end(), registers); it != sregex_iterator(); ++it) { + EXPECT_EQ(count, stoul((*it)[1].str())); + EXPECT_EQ(0x40000000UL + count * 4U, stoul((*it)[2].str(), nullptr, 16)); + count++; + } + EXPECT_EQ(256U, count); + for(unsigned bit = 0; bit < 8; bit++) { + SCOPED_TRACE(bit); + const string value = to_string(bit); + const regex field("\\bPIN" + value + R"(\b[^\n]*\[ )" + value + R"( \.\.\. )" + value + + R"(\][^\n]*Bits: 1)"); + const auto matches = SvdConvTestUtils::FindRegex(listing, field); + EXPECT_EQ(256U, matches.size()); + } +} + #ifndef _WIN32 TEST_F(SvdConvIntegTests, CheckSfrUnsupportedPlatform) { const string inFile = SvdConvIntegTestEnv::localtestdata_dir + "/option_n/option_n.svd"; diff --git a/tools/svdconv/test/unittests/CMakeLists.txt b/tools/svdconv/test/unittests/CMakeLists.txt index 0ae7d392d..cd3e1bfe1 100644 --- a/tools/svdconv/test/unittests/CMakeLists.txt +++ b/tools/svdconv/test/unittests/CMakeLists.txt @@ -1,4 +1,13 @@ -set(TEST_SOURCE_FILES SvdUtilsTest.cpp GeneratorTest.cpp SvdInterruptTest.cpp) +set(TEST_SOURCE_FILES + SvdUtilsTest.cpp + SvdUtilsConversionTest.cpp + GeneratorTest.cpp + SvdInterruptTest.cpp + SvdCpuModelTest.cpp + SvdCExpressionParserTest.cpp + SvdOptionsTest.cpp + SvdModelValidationTest.cpp +) list(TRANSFORM TEST_SOURCE_FILES PREPEND src/) list(TRANSFORM TEST_HEADER_FILES PREPEND src/) diff --git a/tools/svdconv/test/unittests/src/SvdCExpressionParserTest.cpp b/tools/svdconv/test/unittests/src/SvdCExpressionParserTest.cpp new file mode 100644 index 000000000..a4d06437e --- /dev/null +++ b/tools/svdconv/test/unittests/src/SvdCExpressionParserTest.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "SvdCExpressionParser.h" + +#include "gtest/gtest.h" +#include +#include +#include + +using namespace std; + +namespace { + +using Parser = SvdCExpressionParser; +using ExpectedToken = pair; + +TEST(SvdCExpressionParserTest, RecognizesOperatorsAndQualifiers) { + const vector cases{ + {".", Parser::xme_qual}, {"->", Parser::xme_qual}, {"-", Parser::xme_minus}, + {"(", Parser::xme_openbr}, {")", Parser::xme_closebr}, + {"[", Parser::xme_opendim}, {"]", Parser::xme_closedim}, + {"+", Parser::xme_plus}, {"*", Parser::xme_mul}, {"/", Parser::xme_div}, + {"^", Parser::xme_xor}, {"!", Parser::xme_not}, {"~", Parser::xme_compl}, + {"<", Parser::xme_lt}, {"<<", Parser::xme_lsh}, {">>", Parser::xme_rsh}, + {"&", Parser::xme_and}, {"&&", Parser::xme_land}, + {"|", Parser::xme_or}, {"||", Parser::xme_lor}, + {"=", Parser::xme_asn}, {"==", Parser::xme_equ}, + }; + for(const auto& [text, type] : cases) { + SCOPED_TRACE(text); + Parser parser(text); + ASSERT_TRUE(parser.Parse()); + const auto& tokens = parser.GetTokenList(); + ASSERT_EQ(1U, tokens.size()); + EXPECT_EQ(type, tokens.front().type); + EXPECT_EQ(text, tokens.front().text); + } +} + +TEST(SvdCExpressionParserTest, PreservesTokenOrderAndTextInRegisterExpression) { + Parser parser(" \t(GPIO1->CTRL.value & 0x10) == _state2[3]\r\n"); + ASSERT_TRUE(parser.Parse()); + const vector expected{ + {"(", Parser::xme_openbr}, {"GPIO1", Parser::xme_identi}, {"->", Parser::xme_qual}, + {"CTRL", Parser::xme_identi}, {".", Parser::xme_qual}, {"value", Parser::xme_identi}, + {"&", Parser::xme_and}, {"0x10", Parser::xme_const}, {")", Parser::xme_closebr}, + {"==", Parser::xme_equ}, {"_state2", Parser::xme_identi}, {"[", Parser::xme_opendim}, + {"3", Parser::xme_const}, {"]", Parser::xme_closedim}, + }; + const auto& tokens = parser.GetTokenList(); + ASSERT_EQ(expected.size(), tokens.size()); + auto actual = tokens.begin(); + for(const auto& [text, type] : expected) { + SCOPED_TRACE(text); + EXPECT_EQ(type, actual->type); + EXPECT_EQ(text, actual->text); + ++actual; + } +} + +TEST(SvdCExpressionParserTest, HandlesEmptyInputAndTokensAtEndOfInput) { + for(const auto* text : {"", " \t\r\n"}) { + Parser parser(text); + ASSERT_TRUE(parser.Parse()); + EXPECT_TRUE(parser.GetTokenList().empty()); + } + const vector cases{ + {"REGISTER_2", Parser::xme_identi}, {"42", Parser::xme_const}, {"0XFF", Parser::xme_const}, + }; + for(const auto& [text, type] : cases) { + SCOPED_TRACE(text); + Parser parser(text); + ASSERT_TRUE(parser.Parse()); + const auto& tokens = parser.GetTokenList(); + ASSERT_EQ(1U, tokens.size()); + EXPECT_EQ(type, tokens.front().type); + EXPECT_EQ(text, tokens.front().text); + } +} + +} // namespace diff --git a/tools/svdconv/test/unittests/src/SvdCpuModelTest.cpp b/tools/svdconv/test/unittests/src/SvdCpuModelTest.cpp new file mode 100644 index 000000000..d11b59560 --- /dev/null +++ b/tools/svdconv/test/unittests/src/SvdCpuModelTest.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "SvdItem.h" +#include "SvdCpu.h" +#include "SvdInterrupt.h" +#include "SvdSauRegion.h" +#include "ErrLog.h" +#include "XMLTree.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include + +using namespace std; + +namespace { + +class SvdCpuModelTest : public testing::Test, public IErrConsumer { +protected: + void SetUp() override { + const auto log = ErrLog::Get(); + m_previousConsumer = log->SetErrConsumer(this); + m_previousQuiet = log->IsQuietMode(); + m_previousErrors = log->GetErrCnt(); + m_previousWarnings = log->GetWarnCnt(); + log->SetQuietMode(false); + log->ResetMsgCount(); + } + + void TearDown() override { + const auto log = ErrLog::Get(); + log->SetErrConsumer(m_previousConsumer); + log->SetQuietMode(m_previousQuiet); + log->ResetMsgCount(); + for(int i = 0; i < m_previousErrors; i++) { + log->IncErrCnt(); + } + for(int i = 0; i < m_previousWarnings; i++) { + log->IncWarnCnt(); + } + } + + bool Consume(const PdscMsg& msg, const string&) override { + m_messages.push_back(msg.GetMsgNum()); + return true; + } + + static void AddRequiredFields(XMLTreeElement& element, const char* name = "CM33", + const char* revision = "r0p0", const char* endian = "little", + const char* priorityBits = "4") { + element.CreateElement("name", name); + element.CreateElement("revision", revision); + element.CreateElement("endian", endian); + element.CreateElement("nvicPrioBits", priorityBits); + } + + static XMLTreeElement* AddSauConfig(XMLTreeElement& cpuElement, const char* count) { + if(count) { + cpuElement.CreateElement("sauNumRegions", count); + } + const auto config = cpuElement.CreateElement("sauRegionsConfig"); + config->AddAttribute("enabled", "true"); + config->AddAttribute("protectionWhenDisabled", "n"); + const auto region = config->CreateElement("region"); + region->AddAttribute("name", "Application"); + region->CreateElement("base", "0x10000000"); + region->CreateElement("limit", "0x10000FFF"); + region->CreateElement("access", "n"); + return config; + } + + bool HasMessage(const string& id) const { + return find(m_messages.begin(), m_messages.end(), id) != m_messages.end(); + } + + vector m_messages; + +private: + IErrConsumer* m_previousConsumer = nullptr; + bool m_previousQuiet = false; + int m_previousErrors = 0; + int m_previousWarnings = 0; +}; + +TEST_F(SvdCpuModelTest, PreservesParsedRevisionAndEndian) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM33", "r2p3", "big"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_EQ(0x0203U, cpu.GetRevision()); + EXPECT_EQ("r2p3", cpu.GetRevisionStr()); + EXPECT_EQ(SvdTypes::Endian::BIG, cpu.GetEndian()); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdCpuModelTest, InvalidRevisionReportsParseAndMissingRevisionDiagnostics) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM33", "r256p0"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_TRUE(HasMessage("M204")); + EXPECT_TRUE(HasMessage("M325")); +} + +TEST_F(SvdCpuModelTest, MissingRequiredMetadataReportsDefaults) { + XMLTreeElement element(nullptr, "cpu"); + element.CreateElement("name", "CM33"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_TRUE(HasMessage("M325")); + EXPECT_TRUE(HasMessage("M326")); + EXPECT_TRUE(HasMessage("M327")); + EXPECT_EQ(SvdTypes::Endian::LITTLE, cpu.GetEndian()); + EXPECT_EQ(4U, cpu.GetNvicPrioBits()); +} + +TEST_F(SvdCpuModelTest, ChecksNvicPriorityWidthBoundaries) { + for(const auto bits : {1U, 2U, 8U, 9U}) { + SCOPED_TRACE(bits); + m_messages.clear(); + XMLTreeElement element(nullptr, "cpu"); + const auto value = to_string(bits); + AddRequiredFields(element, "CM33", "r0p0", "little", value.c_str()); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + const bool valid = bits == 2U || bits == 8U; + EXPECT_EQ(!valid, HasMessage("M327")); + EXPECT_EQ(valid ? bits : 4U, cpu.GetNvicPrioBits()); + } +} + +TEST_F(SvdCpuModelTest, OptionalFeaturesRetainExplicitEnableAndDisableDeclarations) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM55"); + element.CreateElement("mpuPresent", "true"); + element.CreateElement("fpuPresent", "false"); + element.CreateElement("fpuDP", "false"); + element.CreateElement("icachePresent", "true"); + element.CreateElement("dcachePresent", "false"); + element.CreateElement("itcmPresent", "true"); + element.CreateElement("dtcmPresent", "false"); + element.CreateElement("dspPresent", "true"); + element.CreateElement("mvePresent", "true"); + element.CreateElement("mveFP", "false"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_TRUE(cpu.GetMpuPresent()); + EXPECT_FALSE(cpu.GetFpuPresent()); + EXPECT_FALSE(cpu.GetFpuDP()); + EXPECT_TRUE(cpu.GetIcachePresent()); + EXPECT_FALSE(cpu.GetDcachePresent()); + EXPECT_TRUE(cpu.GetItcmPresent()); + EXPECT_FALSE(cpu.GetDtcmPresent()); + EXPECT_TRUE(cpu.GetDspPresent()); + EXPECT_TRUE(cpu.GetMvePresent()); + EXPECT_FALSE(cpu.GetMveFP()); + // Explicit false values must still override generated CMSIS feature defaults. + const auto& force = cpu.GetCmsisCfgForce(); + EXPECT_TRUE(force.bMpuPresent); + EXPECT_TRUE(force.bFpuPresent); + EXPECT_TRUE(force.bFpuDP); + EXPECT_TRUE(force.bIcachePresent); + EXPECT_TRUE(force.bDcachePresent); + EXPECT_TRUE(force.bItcmPresent); + EXPECT_TRUE(force.bDtcmPresent); + EXPECT_TRUE(force.bDspPresent); + EXPECT_TRUE(force.bMvePresent); + EXPECT_TRUE(force.bMveFP); + EXPECT_FALSE(force.bPmuPresent); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdCpuModelTest, MalformedFeatureAndNumericDeclarationsReportParseErrors) { + for(const auto tag : {"mpuPresent", "fpuPresent", "icachePresent", "dcachePresent", "itcmPresent", + "dtcmPresent", "dspPresent", "mvePresent", "mveFP", "deviceNumInterrupts", + "sauNumRegions", "pmuNumEventCnt"}) { + SCOPED_TRACE(tag); + m_messages.clear(); + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM55"); + element.CreateElement(tag, "invalid"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + EXPECT_TRUE(HasMessage("M202")); + } +} + +TEST_F(SvdCpuModelTest, AcceptsPmuCounterCountsForSupportedCpu) { + for(const auto count : {2U, 31U}) { + SCOPED_TRACE(count); + m_messages.clear(); + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM55"); + element.CreateElement("pmuPresent", "true"); + element.CreateElement("pmuNumEventCnt", to_string(count)); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_TRUE(cpu.GetPmuPresent()); + EXPECT_EQ(count, cpu.GetPmuNumEventCounters()); + EXPECT_TRUE(cpu.GetCmsisCfgForce().bPmuPresent); + EXPECT_TRUE(m_messages.empty()); + } +} + +TEST_F(SvdCpuModelTest, InvalidPmuCounterCountsDisablePmu) { + for(const auto count : {0U, 1U, 33U}) { + SCOPED_TRACE(count); + m_messages.clear(); + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM55"); + element.CreateElement("pmuPresent", "true"); + element.CreateElement("pmuNumEventCnt", to_string(count)); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_FALSE(cpu.GetPmuPresent()); + EXPECT_TRUE(HasMessage("M384")); + EXPECT_FALSE(HasMessage("M385")); + } +} + +TEST_F(SvdCpuModelTest, UnsupportedCpuDisablesPmuAndItsHeaderOverride) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM3"); + element.CreateElement("pmuPresent", "true"); + element.CreateElement("pmuNumEventCnt", "2"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_FALSE(cpu.GetPmuPresent()); + EXPECT_FALSE(cpu.GetCmsisCfgForce().bPmuPresent); + EXPECT_TRUE(HasMessage("M385")); + EXPECT_FALSE(HasMessage("M384")); +} + +TEST_F(SvdCpuModelTest, PmuCounterCountRequiresPmuPresence) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM55"); + element.CreateElement("pmuNumEventCnt", "2"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_FALSE(cpu.GetPmuPresent()); + EXPECT_TRUE(HasMessage("M383")); +} + +TEST_F(SvdCpuModelTest, VendorSystickConfigurationOmitsOnlySystick) { + for(const bool vendorSystick : {false, true}) { + SCOPED_TRACE(vendorSystick); + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + element.CreateElement("vendorSystickConfig", vendorSystick ? "true" : "false"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + const auto& interrupts = cpu.GetInterruptList(); + EXPECT_EQ(vendorSystick ? 0U : 1U, interrupts.count(15U)); + ASSERT_EQ(1U, interrupts.count(14U)); + EXPECT_EQ("PendSV", interrupts.at(14U)->GetName()); + ASSERT_EQ(1U, interrupts.count(7U)); + EXPECT_EQ("SecureFault", interrupts.at(7U)->GetName()); + EXPECT_TRUE(m_messages.empty()); + } +} + +TEST_F(SvdCpuModelTest, BaselineCpuOmitsMainlineFaultHandlers) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element, "CM0"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + const auto& interrupts = cpu.GetInterruptList(); + ASSERT_EQ(1U, interrupts.count(3U)); + EXPECT_EQ("HardFault", interrupts.at(3U)->GetName()); + for(const auto number : {4U, 5U, 6U, 7U, 12U}) { + EXPECT_EQ(0U, interrupts.count(number)) << number; + } + EXPECT_EQ(1U, interrupts.count(15U)); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdCpuModelTest, ParsesSauRegionAttributesAddressesAndAccess) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + const auto configElement = AddSauConfig(element, "2"); + const auto disabledRegion = configElement->CreateElement("region"); + disabledRegion->AddAttribute("name", "Gateway"); + disabledRegion->AddAttribute("enabled", "false"); + disabledRegion->CreateElement("base", "0x20000000"); + disabledRegion->CreateElement("limit", "0x20001FFF"); + disabledRegion->CreateElement("access", "c"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + const auto config = cpu.GetSauRegionsConfig(); + ASSERT_NE(nullptr, config); + EXPECT_TRUE(config->IsValid()); + EXPECT_TRUE(config->GetEnabled()); + EXPECT_EQ(SvdTypes::ProtectionType::NONSECURE, config->GetProtectionWhenDisabled()); + EXPECT_TRUE(cpu.GetCmsisCfgForce().bSauPresent); + const auto& children = config->GetChildren(); + ASSERT_EQ(2U, children.size()); + const auto application = dynamic_cast(children.front()); + const auto gateway = dynamic_cast(children.back()); + ASSERT_NE(nullptr, application); + ASSERT_NE(nullptr, gateway); + EXPECT_TRUE(application->GetEnabled()); + EXPECT_EQ("Application", application->GetName()); + EXPECT_EQ(0x10000000U, application->GetBase()); + EXPECT_EQ(0x10000FFFU, application->GetLimit()); + EXPECT_EQ(SvdTypes::SauAccessType::NONSECURE, application->GetAccessType()); + EXPECT_FALSE(gateway->GetEnabled()); + EXPECT_EQ("Gateway", gateway->GetName()); + EXPECT_EQ(0x20000000U, gateway->GetBase()); + EXPECT_EQ(0x20001FFFU, gateway->GetLimit()); + EXPECT_EQ(SvdTypes::SauAccessType::SECURE, gateway->GetAccessType()); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdCpuModelTest, SauConfigurationRequiresRegionCount) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + AddSauConfig(element, nullptr); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + ASSERT_NE(nullptr, cpu.GetSauRegionsConfig()); + EXPECT_FALSE(cpu.GetSauRegionsConfig()->IsValid()); + EXPECT_TRUE(HasMessage("M363")); +} + +TEST_F(SvdCpuModelTest, SauConfigurationWithZeroRegionsIsInvalid) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + AddSauConfig(element, "0"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + ASSERT_NE(nullptr, cpu.GetSauRegionsConfig()); + EXPECT_FALSE(cpu.GetSauRegionsConfig()->IsValid()); + EXPECT_TRUE(HasMessage("M387")); +} + +TEST_F(SvdCpuModelTest, SauCountOverflowInvalidatesDependentConfiguration) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + AddSauConfig(element, "256"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + ASSERT_NE(nullptr, cpu.GetSauRegionsConfig()); + EXPECT_FALSE(cpu.GetSauRegionsConfig()->IsValid()); + EXPECT_EQ(SvdItem::VALUE32_NOT_INIT, cpu.GetSauNumRegions()); + EXPECT_TRUE(HasMessage("M364")); +} + +TEST_F(SvdCpuModelTest, ExcessSauRegionDefinitionsAreDiagnosed) { + XMLTreeElement element(nullptr, "cpu"); + AddRequiredFields(element); + const auto config = AddSauConfig(element, "1"); + const auto secondRegion = config->CreateElement("region"); + secondRegion->CreateElement("base", "0x20000000"); + secondRegion->CreateElement("limit", "0x20000FFF"); + secondRegion->CreateElement("access", "n"); + SvdCpu cpu(nullptr); + ASSERT_TRUE(cpu.Construct(&element)); + + EXPECT_TRUE(HasMessage("M391")); +} + +} // namespace diff --git a/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp b/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp new file mode 100644 index 000000000..90ad01f95 --- /dev/null +++ b/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "SvdItem.h" +#include "SvdAddressBlock.h" +#include "SvdDimension.h" +#include "SvdEnum.h" +#include "SvdField.h" +#include "SvdRegister.h" +#include "ErrLog.h" +#include "XMLTree.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace { + +class SvdModelValidationTest : public testing::Test, public IErrConsumer { +protected: + void SetUp() override { + const auto log = ErrLog::Get(); + m_previousConsumer = log->SetErrConsumer(this); + m_previousQuiet = log->IsQuietMode(); + m_previousErrors = log->GetErrCnt(); + m_previousWarnings = log->GetWarnCnt(); + log->SetQuietMode(false); + log->ResetMsgCount(); + } + + void TearDown() override { + const auto log = ErrLog::Get(); + log->SetErrConsumer(m_previousConsumer); + log->SetQuietMode(m_previousQuiet); + log->ResetMsgCount(); + for(int i = 0; i < m_previousErrors; i++) { + log->IncErrCnt(); + } + for(int i = 0; i < m_previousWarnings; i++) { + log->IncWarnCnt(); + } + } + + bool Consume(const PdscMsg& msg, const string&) override { + m_messages.push_back(msg.GetMsgNum()); + return true; + } + + bool HasMessage(const string& id) const { + return find(m_messages.begin(), m_messages.end(), id) != m_messages.end(); + } + + static void AddAddressBlockFields(XMLTreeElement& element, const char* offset = "0", + const char* size = "4", const char* usage = "registers") { + for(const auto& [tag, value] : {pair{"offset", offset}, pair{"size", size}, pair{"usage", usage}}) { + if(value) { + element.CreateElement(tag, value); + } + } + } + + static void AddEnum(XMLTreeElement& container, const char* value, const char* isDefault = nullptr) { + const auto element = container.CreateElement("enumeratedValue"); + element->CreateElement("name", "MODE"); + element->CreateElement("description", "Operating mode."); + if(value) { + element->CreateElement("value", value); + } + if(isDefault) { + element->CreateElement("isDefault", isDefault); + } + } + + static void ConfigureDimension(SvdRegister& reg, const char* count, const char* indices) { + reg.SetName("CHANNEL%s"); + reg.SetDescription("Control channel %s."); + for(const auto& [tag, value] : {pair{"dim", count}, pair{"dimIncrement", "4"}, pair{"dimIndex", indices}}) { + XMLTreeElement element(nullptr, tag); + element.SetText(value); + ASSERT_TRUE(reg.ProcessXmlElement(&element)); + } + ASSERT_NE(nullptr, reg.GetDimension()); + } + + vector m_messages; + +private: + IErrConsumer* m_previousConsumer = nullptr; + bool m_previousQuiet = false; + int m_previousErrors = 0; + int m_previousWarnings = 0; +}; + +TEST_F(SvdModelValidationTest, AddressBlockRequiresOffsetSizeAndUsage) { + struct MissingField { + const char* offset; + const char* size; + const char* usage; + const char* diagnostic; + }; + for(const auto& values : {MissingField{nullptr, "4", "registers", "M314"}, + MissingField{"0", nullptr, "registers", "M314"}, + MissingField{"0", "4", nullptr, "M359"}}) { + SCOPED_TRACE(values.diagnostic); + m_messages.clear(); + XMLTreeElement element(nullptr, "addressBlock"); + AddAddressBlockFields(element, values.offset, values.size, values.usage); + SvdAddressBlock block(nullptr); + ASSERT_TRUE(block.Construct(&element)); + EXPECT_FALSE(block.IsValid()); + EXPECT_TRUE(HasMessage(values.diagnostic)); + } +} + +TEST_F(SvdModelValidationTest, AddressBlockRejectsZeroSize) { + XMLTreeElement element(nullptr, "addressBlock"); + AddAddressBlockFields(element, "0", "0"); + SvdAddressBlock block(nullptr); + ASSERT_TRUE(block.Construct(&element)); + + EXPECT_FALSE(block.IsValid()); + EXPECT_TRUE(HasMessage("M315")); +} + +TEST_F(SvdModelValidationTest, AddressBlockReportsMalformedFields) { + for(const auto* tag : {"offset", "size", "usage"}) { + SCOPED_TRACE(tag); + m_messages.clear(); + XMLTreeElement element(nullptr, "addressBlock"); + AddAddressBlockFields(element, string(tag) == "offset" ? "invalid" : "0", + string(tag) == "size" ? "invalid" : "4", + string(tag) == "usage" ? "invalid" : "registers"); + SvdAddressBlock block(nullptr); + ASSERT_TRUE(block.Construct(&element)); + EXPECT_TRUE(HasMessage("M202")); + } +} + +TEST_F(SvdModelValidationTest, LargeAddressBlocksWarnWithoutLosingCompatibility) { + for(const bool testOffset : {false, true}) { + for(const auto value : {0x1000000U, 0x1000001U}) { + SCOPED_TRACE(testOffset ? "offset" : "size"); + SCOPED_TRACE(value); + m_messages.clear(); + XMLTreeElement element(nullptr, "addressBlock"); + const auto text = to_string(value); + AddAddressBlockFields(element, testOffset ? text.c_str() : "0", testOffset ? "4" : text.c_str()); + SvdAddressBlock block(nullptr); + ASSERT_TRUE(block.Construct(&element)); + + EXPECT_TRUE(block.IsValid()); + EXPECT_EQ(value > 0x1000000U, HasMessage("M360")); + EXPECT_EQ(value, testOffset ? block.GetOffset() : block.GetSize()); + } + } +} + +TEST_F(SvdModelValidationTest, NonDefaultEnumRequiresValue) { + SvdField field(nullptr); + SvdEnumContainer container(&field); + XMLTreeElement element(nullptr, "enumeratedValues"); + AddEnum(element, nullptr); + ASSERT_TRUE(container.Construct(&element)); + + ASSERT_EQ(1U, container.GetChildCount()); + EXPECT_FALSE(container.GetChildren().front()->IsValid()); + EXPECT_TRUE(HasMessage("M369")); +} + +TEST_F(SvdModelValidationTest, DefaultEnumNeedsNoNumericValueAndRegistersWithContainer) { + SvdField field(nullptr); + SvdEnumContainer container(&field); + XMLTreeElement element(nullptr, "enumeratedValues"); + AddEnum(element, nullptr, "true"); + ASSERT_TRUE(container.Construct(&element)); + + ASSERT_EQ(1U, container.GetChildCount()); + const auto value = dynamic_cast(container.GetChildren().front()); + ASSERT_NE(nullptr, value); + EXPECT_TRUE(value->IsValid()); + EXPECT_TRUE(value->IsDefault()); + EXPECT_FALSE(value->GetValue().bValid); + EXPECT_EQ(value, container.GetDefaultValue()); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdModelValidationTest, MalformedEnumValueIsInvalid) { + SvdField field(nullptr); + SvdEnumContainer container(&field); + XMLTreeElement element(nullptr, "enumeratedValues"); + AddEnum(element, "invalid"); + ASSERT_TRUE(container.Construct(&element)); + + ASSERT_EQ(1U, container.GetChildCount()); + EXPECT_FALSE(container.GetChildren().front()->IsValid()); + EXPECT_TRUE(HasMessage("M202")); +} + +TEST_F(SvdModelValidationTest, DimensionArrayIndexDisallowsDefaultEnum) { + SvdRegister reg(nullptr); + SvdEnumContainer container(®); + XMLTreeElement element(nullptr, "dimArrayIndex"); + AddEnum(element, nullptr, "true"); + ASSERT_TRUE(container.Construct(&element)); + + ASSERT_EQ(1U, container.GetChildCount()); + EXPECT_FALSE(container.GetChildren().front()->IsValid()); + EXPECT_EQ(nullptr, container.GetDefaultValue()); + EXPECT_TRUE(HasMessage("M231")); +} + +TEST_F(SvdModelValidationTest, WildcardEnumExpandsIntoDistinctNamedValues) { + SvdField field(nullptr); + SvdEnumContainer container(&field); + XMLTreeElement element(nullptr, "enumeratedValues"); + AddEnum(element, "#10x"); + ASSERT_TRUE(container.Construct(&element)); + + ASSERT_EQ(2U, container.GetChildCount()); + map values; + for(const auto child : container.GetChildren()) { + const auto value = dynamic_cast(child); + ASSERT_NE(nullptr, value); + EXPECT_TRUE(value->IsValid()); + EXPECT_TRUE(value->GetValue().bValid); + EXPECT_EQ("Operating mode.", value->GetDescription()); + values.emplace(value->GetName(), value->GetValue().u32); + } + const map expected{{"MODE_4", 4U}, {"MODE_5", 5U}}; + EXPECT_EQ(expected, values); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdModelValidationTest, ExplicitDimensionIndicesGenerateNames) { + SvdRegister reg(nullptr); + ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", "LEFT, CENTER, RIGHT")); + const auto dim = reg.GetDimension(); + ASSERT_TRUE(dim->CalculateDim()); + + const list expected{"LEFT", "CENTER", "RIGHT"}; + EXPECT_EQ(expected, dim->GetDimIndexList()); + EXPECT_EQ("CHANNELCENTER", dim->CreateName("CENTER")); + EXPECT_EQ("Control channel CENTER.", dim->CreateDescription("CENTER")); + EXPECT_TRUE(reg.IsValid()); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdModelValidationTest, AlphabeticDimensionRangeExpandsInOrder) { + SvdRegister reg(nullptr); + ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", "A-C")); + const auto dim = reg.GetDimension(); + ASSERT_TRUE(dim->CalculateDim()); + + const list expected{"A", "B", "C"}; + EXPECT_EQ(expected, dim->GetDimIndexList()); + EXPECT_EQ("CHANNELA", dim->CreateName("A")); + EXPECT_EQ("CHANNELC", dim->CreateName("C")); + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdModelValidationTest, InvalidDimensionRangesDoNotProduceIndices) { + for(const auto* indices : {"AA-AC", "C-A", "A-D", "0-3"}) { + SCOPED_TRACE(indices); + SvdRegister reg(nullptr); + ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", indices)); + const auto dim = reg.GetDimension(); + EXPECT_FALSE(dim->CalculateDimIndex()); + EXPECT_TRUE(dim->GetDimIndexList().empty()); + } +} + +TEST_F(SvdModelValidationTest, DimensionListsDiagnoseCountMismatchAndDuplicateIndices) { + for(const auto& [indices, diagnostic] : {pair{"A,B", "M308"}, pair{"A,A,B", "M336"}}) { + SCOPED_TRACE(indices); + m_messages.clear(); + SvdRegister reg(nullptr); + ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", indices)); + ASSERT_TRUE(reg.GetDimension()->CalculateDim()); + EXPECT_TRUE(HasMessage(diagnostic)); + } +} + +} // namespace diff --git a/tools/svdconv/test/unittests/src/SvdOptionsTest.cpp b/tools/svdconv/test/unittests/src/SvdOptionsTest.cpp new file mode 100644 index 000000000..b7cbb8c1e --- /dev/null +++ b/tools/svdconv/test/unittests/src/SvdOptionsTest.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "ParseOptions.h" +#include "SvdOptions.h" +#include "ErrLog.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace { + +class ScopedConsoleCapture { +public: + ScopedConsoleCapture() : + m_previousOutput(cout.rdbuf(m_output.rdbuf())), + m_previousError(cerr.rdbuf(m_error.rdbuf())) { + } + + ~ScopedConsoleCapture() { + cout.rdbuf(m_previousOutput); + cerr.rdbuf(m_previousError); + } + + string Output() const { return m_output.str(); } + string Error() const { return m_error.str(); } + +private: + ostringstream m_output; + ostringstream m_error; + streambuf* m_previousOutput; + streambuf* m_previousError; +}; + +class SvdOptionsTest : public testing::Test { +protected: + void SetUp() override { + const auto log = ErrLog::Get(); + m_previousLevel = log->GetLevel(); + m_previousQuiet = log->IsQuietMode(); + m_previousStrict = log->IsStrictMode(); + m_previousAllowSuppressError = log->IsAllowSuppressError(); + m_previousErrors = log->GetErrCnt(); + m_previousWarnings = log->GetWarnCnt(); + log->SetLevel(MsgLevel::LEVEL_WARNING3); + log->SetQuietMode(false); + log->SetStrictMode(false); + log->SetAllowSuppressError(false); + log->ResetMsgCount(); + } + + void TearDown() override { + const auto log = ErrLog::Get(); + log->SetLevel(m_previousLevel); + log->SetQuietMode(m_previousQuiet); + log->SetStrictMode(m_previousStrict); + log->SetAllowSuppressError(m_previousAllowSuppressError); + log->ResetMsgCount(); + for(int i = 0; i < m_previousErrors; i++) { + log->IncErrCnt(); + } + for(int i = 0; i < m_previousWarnings; i++) { + log->IncWarnCnt(); + } + } + + ParseOptions::Result Parse(initializer_list options) { + vector args{"SVDConv"}; + args.insert(args.end(), options.begin(), options.end()); + ParseOptions parser(m_options); + ScopedConsoleCapture capture; + const auto result = parser.Parse(static_cast(args.size()), args.data()); + m_output = capture.Output(); + m_error = capture.Error(); + return result; + } + + SvdOptions m_options; + string m_output; + string m_error; + +private: + MsgLevel m_previousLevel = MsgLevel::LEVEL_WARNING3; + bool m_previousQuiet = false; + bool m_previousStrict = false; + bool m_previousAllowSuppressError = false; + int m_previousErrors = 0; + int m_previousWarnings = 0; +}; + +TEST_F(SvdOptionsTest, TokenizesOptionFileLinesWithoutFilesystemAccess) { + const vector>> cases{ + {"", {}}, + {" \t\r ", {}}, + {"# comment", {}}, + {" --generate=header\t--fields=enum ", {"--generate=header", "--fields=enum"}}, + {"--generate=header # ignored option", {"--generate=header"}}, + {"--generate=header#comment", {"--generate=header"}}, + }; + ParseOptions parser(m_options); + for(const auto& [line, expected] : cases) { + SCOPED_TRACE(line); + vector options; + EXPECT_EQ(ParseOptions::Result::Ok, parser.ParseOptsFileLine(line, options)); + EXPECT_EQ(expected, options); + } + + for(const auto* value : {"my header", "my # header"}) { + SCOPED_TRACE(value); + vector options; + const string line = string("-n \"") + value + "\" # comment"; + EXPECT_EQ(ParseOptions::Result::Ok, parser.ParseOptsFileLine(line, options)); + ASSERT_EQ(2U, options.size()); + EXPECT_EQ("-n", options[0]); + EXPECT_TRUE(options[1] == value || options[1] == string("\"") + value + "\"") << options[1]; + } +} + +TEST_F(SvdOptionsTest, AppendsOptionFileTokensToExistingArguments) { + ParseOptions parser(m_options); + vector options{"SVDConv", "--generate=header"}; + EXPECT_EQ(ParseOptions::Result::Ok, parser.ParseOptsFileLine("--fields=enum", options)); + EXPECT_EQ((vector{"SVDConv", "--generate=header", "--fields=enum"}), options); +} + +TEST_F(SvdOptionsTest, ShowsHelpWithoutStartingConversion) { + for(const auto& arguments : {initializer_list{}, {"--help"}, {"-h"}}) { + EXPECT_EQ(ParseOptions::Result::ExitNoError, Parse(arguments)); + EXPECT_NE(string::npos, m_output.find("--generate")); + EXPECT_NE(string::npos, m_output.find("--fields")); + EXPECT_TRUE(m_error.empty()); + EXPECT_TRUE(m_options.GetSvdFullpath().empty()); + } +} + +TEST_F(SvdOptionsTest, ShowsVersionWithoutStartingConversion) { + const auto version = m_options.GetVersion(); + ASSERT_FALSE(version.empty()); + for(const auto* option : {"--version", "-V"}) { + SCOPED_TRACE(option); + EXPECT_EQ(ParseOptions::Result::ExitNoError, Parse({option})); + EXPECT_NE(string::npos, m_output.find(version)); + EXPECT_TRUE(m_error.empty()); + EXPECT_TRUE(m_options.GetSvdFullpath().empty()); + } +} + +TEST_F(SvdOptionsTest, RejectsUnknownOptionsAndMissingOptionValues) { + for(const auto* option : {"--not-an-option", "--generate", "--fields", "-o"}) { + SCOPED_TRACE(option); + EXPECT_EQ(ParseOptions::Result::Error, Parse({option})); + EXPECT_FALSE(m_error.empty()); + EXPECT_TRUE(m_output.empty()); + } +} + +TEST_F(SvdOptionsTest, AppliesExplicitWarningLevels) { + const vector> cases{ + {"-w0", MsgLevel::LEVEL_ERROR}, + {"-w1", MsgLevel::LEVEL_WARNING}, + {"-w2", MsgLevel::LEVEL_WARNING2}, + {"-w3", MsgLevel::LEVEL_WARNING3}, + {"-wall", MsgLevel::LEVEL_WARNING3}, + }; + for(const auto& [option, expected] : cases) { + SCOPED_TRACE(option); + ErrLog::Get()->SetLevel(MsgLevel::LEVEL_PROGRESS); + EXPECT_EQ(ParseOptions::Result::Ok, Parse({option})); + EXPECT_EQ(expected, ErrLog::Get()->GetLevel()); + EXPECT_TRUE(m_error.empty()); + } +} + +TEST_F(SvdOptionsTest, EnablesVerboseDiagnostics) { + EXPECT_EQ(ParseOptions::Result::Ok, Parse({"--verbose"})); + EXPECT_EQ(MsgLevel::LEVEL_PROGRESS, ErrLog::Get()->GetLevel()); +} + +TEST_F(SvdOptionsTest, SuppressesWarningDiagnostics) { + EXPECT_EQ(ParseOptions::Result::Ok, Parse({"--suppress-warnings"})); + EXPECT_EQ(MsgLevel::LEVEL_ERROR, ErrLog::Get()->GetLevel()); +} + +TEST_F(SvdOptionsTest, EnablesQuietMode) { + EXPECT_EQ(ParseOptions::Result::Ok, Parse({"--quiet"})); + EXPECT_TRUE(ErrLog::Get()->IsQuietMode()); +} + +TEST_F(SvdOptionsTest, EnablesStrictMode) { + EXPECT_EQ(ParseOptions::Result::Ok, Parse({"--strict"})); + EXPECT_TRUE(ErrLog::Get()->IsStrictMode()); +} + +TEST_F(SvdOptionsTest, AllowsErrorSuppressionWithoutChangingSuppressionSets) { + EXPECT_EQ(ParseOptions::Result::Ok, Parse({"--allow-suppress-error"})); + EXPECT_TRUE(ErrLog::Get()->IsAllowSuppressError()); +} + +TEST_F(SvdOptionsTest, RejectsEmptyLogAndDiagnosticSuppressionArguments) { + EXPECT_FALSE(m_options.SetLogFile("")); + EXPECT_TRUE(m_options.GetLogPath().empty()); + EXPECT_FALSE(m_options.AddDiagSuppress("")); +} + +TEST_F(SvdOptionsTest, DoesNotChangeWarningLevelWhenVerboseIsDisabled) { + ErrLog::Get()->SetLevel(MsgLevel::LEVEL_WARNING2); + EXPECT_TRUE(m_options.SetVerbose(false)); + EXPECT_EQ(MsgLevel::LEVEL_WARNING2, ErrLog::Get()->GetLevel()); +} + +} // namespace diff --git a/tools/svdconv/test/unittests/src/SvdUtilsConversionTest.cpp b/tools/svdconv/test/unittests/src/SvdUtilsConversionTest.cpp new file mode 100644 index 000000000..8bf115b9e --- /dev/null +++ b/tools/svdconv/test/unittests/src/SvdUtilsConversionTest.cpp @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "SvdUtils.h" +#include "ErrLog.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace { + +class SvdUtilsConversionTest : public testing::Test, public IErrConsumer { +protected: + void SetUp() override { + const auto log = ErrLog::Get(); + m_previousConsumer = log->SetErrConsumer(this); + m_previousQuiet = log->IsQuietMode(); + m_previousErrors = log->GetErrCnt(); + m_previousWarnings = log->GetWarnCnt(); + log->SetQuietMode(false); + log->ResetMsgCount(); + } + + void TearDown() override { + const auto log = ErrLog::Get(); + log->SetErrConsumer(m_previousConsumer); + log->SetQuietMode(m_previousQuiet); + log->ResetMsgCount(); + for(int i = 0; i < m_previousErrors; i++) { + log->IncErrCnt(); + } + for(int i = 0; i < m_previousWarnings; i++) { + log->IncWarnCnt(); + } + } + + bool Consume(const PdscMsg& msg, const string&) override { + m_messages.push_back(msg.GetMsgNum()); + return true; + } + + template + void CheckEnumConversion(bool (*convert)(const string&, Enum&, uint32_t), + initializer_list> values) { + for(const auto& [text, expected] : values) { + SCOPED_TRACE(text); + auto value = Enum::UNDEF; + m_messages.clear(); + EXPECT_TRUE(convert(text, value, 7U)); + EXPECT_EQ(expected, value); + EXPECT_TRUE(m_messages.empty()); + + string upper = text; + SvdUtils::ToUpper(upper); + value = Enum::UNDEF; + EXPECT_TRUE(convert(upper, value, 7U)); + EXPECT_EQ(expected, value); + EXPECT_EQ(m_messages, vector{"M225"}); + } + auto value = Enum::UNDEF; + EXPECT_FALSE(convert("", value, 7U)); + EXPECT_FALSE(convert("unknown", value, 7U)); + EXPECT_EQ(Enum::UNDEF, value); + } + + vector m_messages; + IErrConsumer* m_previousConsumer = nullptr; + bool m_previousQuiet = false; + int m_previousErrors = 0; + int m_previousWarnings = 0; +}; + +TEST_F(SvdUtilsConversionTest, ConvertsNumericRepresentationsWithoutLosingWidth) { + const vector> cases{ + {"0", 0U}, {"4294967296", 0x100000000ULL}, {"0xFFFFFFFFFFFFFFFF", numeric_limits::max()}, + {"0XAbCd", 0xabcdU}, {"0b101010", 42U}, {"#101010", 42U}, {"TRUE", 1U}, {"False", 0U}, + }; + for(const auto& [text, expected] : cases) { + SCOPED_TRACE(text); + uint64_t value = 99U; + EXPECT_TRUE(SvdUtils::ConvertNumber(text, value)); + EXPECT_EQ(expected, value); + } + uint32_t unsignedValue = 0U; + EXPECT_TRUE(SvdUtils::ConvertNumber("0xffffffff", unsignedValue)); + EXPECT_EQ(numeric_limits::max(), unsignedValue); + int32_t signedValue = 0; + EXPECT_TRUE(SvdUtils::ConvertNumber("2147483647", signedValue)); + EXPECT_EQ(numeric_limits::max(), signedValue); +} + +TEST_F(SvdUtilsConversionTest, RejectsMalformedNumbersAndUnsupportedBases) { + for(const auto* text : {"", "0x", "0b", "#", "0xfg", "0b102", "#102", "0b1x", "#1x", "12a", "-1"}) { + SCOPED_TRACE(text); + uint64_t value = 17U; + EXPECT_FALSE(SvdUtils::ConvertNumber(text, value)); + EXPECT_EQ(17U, value); + } + uint64_t value = 0U; + EXPECT_FALSE(SvdUtils::ConvertNumber("12", value, 8U)); + EXPECT_TRUE(SvdUtils::ConvertNumber("ff", value, 16U)); + EXPECT_EQ(255U, value); +} + +TEST_F(SvdUtilsConversionTest, ConvertsBooleanValuesAndRejectsNonBooleans) { + for(const auto* text : {"true", "TRUE", "True", "1"}) { + bool value = false; + EXPECT_TRUE(SvdUtils::ConvertNumber(text, value)) << text; + EXPECT_TRUE(value); + } + for(const auto* text : {"false", "FALSE", "False", "0"}) { + bool value = true; + EXPECT_TRUE(SvdUtils::ConvertNumber(text, value)) << text; + EXPECT_FALSE(value); + } + for(const auto* text : {"", "2", "yes", "0x1", " true"}) { + bool value = true; + EXPECT_FALSE(SvdUtils::ConvertNumber(text, value)) << text; + EXPECT_TRUE(value); + } +} + +TEST_F(SvdUtilsConversionTest, AccumulatesUniqueNumericValues) { + set values{1U}; + EXPECT_TRUE(SvdUtils::ConvertNumber("0x100000000", values)); + EXPECT_TRUE(SvdUtils::ConvertNumber("4294967296", values)); + EXPECT_FALSE(SvdUtils::ConvertNumber("invalid", values)); + EXPECT_FALSE(SvdUtils::ConvertNumber("", values)); + EXPECT_EQ(values, (set{1U, 0x100000000ULL})); +} + +TEST_F(SvdUtilsConversionTest, ExpandsBinaryWildcardsAndEnforcesExpansionLimit) { + set values{99U}; + ASSERT_TRUE(SvdUtils::ConvertNumberXBin("#10x", values)); + EXPECT_EQ(values, (set{4U, 5U})); + ASSERT_TRUE(SvdUtils::ConvertNumberXBin("0B1X0X", values)); + EXPECT_EQ(values, (set{8U, 9U, 12U, 13U})); + ASSERT_TRUE(SvdUtils::ConvertNumberXBin("0x10", values)); + EXPECT_EQ(values, (set{16U})); + ASSERT_TRUE(SvdUtils::ConvertNumberXBin("#xxxxxxxx", values)); + EXPECT_EQ(values.size(), 256U); + EXPECT_EQ(*values.begin(), 0U); + EXPECT_EQ(*values.rbegin(), 255U); + EXPECT_FALSE(SvdUtils::ConvertNumberXBin("#xxxxxxxxx", values)); + EXPECT_TRUE(values.empty()); + EXPECT_EQ(m_messages, vector{"M379"}); + for(const auto* text : {"", "0b", "#", "invalid"}) { + EXPECT_FALSE(SvdUtils::ConvertNumberXBin(text, values)) << text; + } +} + +TEST_F(SvdUtilsConversionTest, ParsesBitRangeAndRejectsMalformedDelimiters) { + uint32_t msb = 0U; + uint32_t lsb = 0U; + EXPECT_TRUE(SvdUtils::ConvertBitRange("[31:16]", msb, lsb)); + EXPECT_EQ(msb, 31U); + EXPECT_EQ(lsb, 16U); + EXPECT_TRUE(SvdUtils::ConvertBitRange("[7:7]", msb, lsb)); + EXPECT_EQ(msb, 7U); + EXPECT_EQ(lsb, 7U); + for(const auto* text : {"", "31:0]", "[31:0", "[[31:0]", "[31:0]]", "[31]", "[:0]", "[31:]", + "[x:0]", "[31:x]"}) { + EXPECT_FALSE(SvdUtils::ConvertBitRange(text, msb, lsb)) << text; + } +} + +TEST_F(SvdUtilsConversionTest, ConvertsCpuRevisionWithoutMixingMajorAndMinor) { + const vector> cases{ + {"r0p0", 0U}, {"r1p2", 0x0102U}, {"R2P1", 0x0201U}, + {"r255p0", 0xff00U}, {"r0p255", 0x00ffU}, {"r255p255", 0xffffU}, + }; + for(const auto& [text, expected] : cases) { + SCOPED_TRACE(text); + uint32_t revision = 0U; + EXPECT_TRUE(SvdUtils::ConvertCpuRevision(text, revision)); + EXPECT_EQ(expected, revision); + } +} + +TEST_F(SvdUtilsConversionTest, RejectsMalformedAndOutOfRangeCpuRevisions) { + for(const auto* text : {"", "1p2", "r1", "rp1", "r1p", "rAp1", "r1pA", "r1p2p3", + "r256p0", "r0p256", "r-1p0"}) { + SCOPED_TRACE(text); + uint32_t revision = 0x1234U; + EXPECT_FALSE(SvdUtils::ConvertCpuRevision(text, revision)); + EXPECT_EQ(0x1234U, revision); + } +} + +TEST_F(SvdUtilsConversionTest, NormalizesCpuTypeNamesAndCortexM0PlusAlias) { + using Cpu = SvdTypes::CpuType; + const vector> cases{ + {"CM0", Cpu::CM0}, {"cm0+", Cpu::CM0PLUS}, {"Cm0Plus", Cpu::CM0PLUS}, + {"cm33", Cpu::CM33}, {"armv81mml", Cpu::V81MML}, {"other", Cpu::OTHER}, + }; + for(const auto& [text, expected] : cases) { + SCOPED_TRACE(text); + auto cpu = Cpu::UNDEF; + EXPECT_TRUE(SvdUtils::ConvertCpuType(text, cpu)); + EXPECT_EQ(expected, cpu); + } + for(const auto* text : {"", "CM999", "not-a-cpu"}) { + auto cpu = Cpu::UNDEF; + EXPECT_FALSE(SvdUtils::ConvertCpuType(text, cpu)) << text; + } +} + +TEST_F(SvdUtilsConversionTest, ConvertsCpuEndianAndDiagnosesNoncanonicalCase) { + using Endian = SvdTypes::Endian; + const vector> cases{ + {"little", Endian::LITTLE}, {"big", Endian::BIG}, + {"selectable", Endian::SELECTABLE}, {"other", Endian::OTHER}, + }; + for(const auto& [text, expected] : cases) { + SCOPED_TRACE(text); + auto endian = Endian::UNDEF; + m_messages.clear(); + EXPECT_TRUE(SvdUtils::ConvertCpuEndian(text, endian, 7U)); + EXPECT_EQ(expected, endian); + EXPECT_TRUE(m_messages.empty()); + + string upper = text; + SvdUtils::ToUpper(upper); + endian = Endian::UNDEF; + EXPECT_TRUE(SvdUtils::ConvertCpuEndian(upper, endian, 7U)); + EXPECT_EQ(expected, endian); + EXPECT_EQ(m_messages, vector{"M225"}); + } + for(const auto* text : {"", "unknown"}) { + auto endian = Endian::UNDEF; + EXPECT_FALSE(SvdUtils::ConvertCpuEndian(text, endian, 7U)) << text; + } +} + +TEST_F(SvdUtilsConversionTest, ConvertsIntegerAndPointerDataTypes) { + for(const auto* baseType : {"uint8_t", "uint16_t", "uint32_t", "uint64_t", + "int8_t", "int16_t", "int32_t", "int64_t"}) { + for(const auto* suffix : {"", "*"}) { + const string expected = string(baseType) + suffix; + SCOPED_TRACE(expected); + string dataType; + EXPECT_TRUE(SvdUtils::ConvertDataType(expected, dataType, 7U)); + EXPECT_EQ(expected, dataType); + + string upper = expected; + SvdUtils::ToUpper(upper); + dataType.clear(); + EXPECT_TRUE(SvdUtils::ConvertDataType(upper, dataType, 7U)); + EXPECT_EQ(expected, dataType); + } + } +} + +TEST_F(SvdUtilsConversionTest, RejectsUnsupportedDataTypesWithoutReplacingPreviousType) { + for(const auto* text : {"", "float", "uint24_t", "int128_t", "uint32_t**", "uint32_tjunk"}) { + SCOPED_TRACE(text); + string dataType = "uint16_t"; + EXPECT_FALSE(SvdUtils::ConvertDataType(text, dataType, 7U)); + EXPECT_EQ("uint16_t", dataType); + } +} + +TEST_F(SvdUtilsConversionTest, DistinguishesGeneralProtectionFromSauProtection) { + using Protection = SvdTypes::ProtectionType; + for(const auto& [text, expected] : vector>{ + {"s", Protection::SECURE}, {"n", Protection::NONSECURE}, {"p", Protection::PRIVILEGED}}) { + SCOPED_TRACE(text); + auto protection = Protection::UNDEF; + EXPECT_TRUE(SvdUtils::ConvertProtectionStringType(text, protection, 7U)); + EXPECT_EQ(expected, protection); + + protection = Protection::UNDEF; + if(expected == Protection::PRIVILEGED) { + EXPECT_FALSE(SvdUtils::ConvertSauProtectionStringType(text, protection, 7U)); + } + else { + EXPECT_TRUE(SvdUtils::ConvertSauProtectionStringType(text, protection, 7U)); + EXPECT_EQ(expected, protection); + } + } + for(const auto* text : {"", "unknown", "secure"}) { + auto protection = Protection::UNDEF; + EXPECT_FALSE(SvdUtils::ConvertProtectionStringType(text, protection, 7U)) << text; + EXPECT_FALSE(SvdUtils::ConvertSauProtectionStringType(text, protection, 7U)) << text; + } + EXPECT_TRUE(m_messages.empty()); +} + +TEST_F(SvdUtilsConversionTest, RejectsNoncanonicalProtectionWithCaseDiagnostic) { + for(const auto* text : {"S", "N", "P"}) { + SCOPED_TRACE(text); + auto protection = SvdTypes::ProtectionType::UNDEF; + m_messages.clear(); + EXPECT_FALSE(SvdUtils::ConvertProtectionStringType(text, protection, 7U)); + EXPECT_EQ(m_messages, vector{"M225"}); + } + for(const auto* text : {"S", "N"}) { + SCOPED_TRACE(text); + auto protection = SvdTypes::ProtectionType::UNDEF; + m_messages.clear(); + EXPECT_FALSE(SvdUtils::ConvertSauProtectionStringType(text, protection, 7U)); + EXPECT_EQ(m_messages, vector{"M225"}); + } +} + +TEST_F(SvdUtilsConversionTest, ConvertsSauAccessAndRejectsEmptyOrNoncanonicalCase) { + using Access = SvdTypes::SauAccessType; + for(const auto& [text, expected] : vector>{ + {"c", Access::SECURE}, {"n", Access::NONSECURE}}) { + SCOPED_TRACE(text); + auto access = Access::UNDEF; + m_messages.clear(); + EXPECT_TRUE(SvdUtils::ConvertSauAccessType(text, access, 7U)); + EXPECT_EQ(expected, access); + EXPECT_TRUE(m_messages.empty()); + + string upper = text; + SvdUtils::ToUpper(upper); + EXPECT_FALSE(SvdUtils::ConvertSauAccessType(upper, access, 7U)); + EXPECT_EQ(m_messages, vector{"M225"}); + } + auto access = Access::UNDEF; + EXPECT_FALSE(SvdUtils::ConvertSauAccessType("", access, 7U)); +} + +TEST_F(SvdUtilsConversionTest, ConvertsAddressBlockAndEnumUsage) { + using Block = SvdTypes::AddrBlockUsage; + CheckEnumConversion(SvdUtils::ConvertAddrBlockUsage, + {{"registers", Block::REGISTERS}, {"buffer", Block::BUFFER}, {"reserved", Block::RESERVED}}); + using Usage = SvdTypes::EnumUsage; + CheckEnumConversion(SvdUtils::ConvertEnumUsage, + {{"read", Usage::READ}, {"write", Usage::WRITE}, {"read-write", Usage::READWRITE}}); +} + +TEST_F(SvdUtilsConversionTest, ConvertsModifiedWriteValues) { + using Write = SvdTypes::ModifiedWriteValue; + CheckEnumConversion(SvdUtils::ConvertModifiedWriteValues, { + {"oneToClear", Write::ONETOCLEAR}, {"oneToSet", Write::ONETOSET}, {"oneToToggle", Write::ONETOTOGGLE}, + {"zeroToClear", Write::ZEROTOCLEAR}, {"zeroToSet", Write::ZEROTOSET}, {"zeroToToggle", Write::ZEROTOTOGGLE}, + {"clear", Write::CLEAR}, {"set", Write::SET}, {"modify", Write::MODIFY}, + }); +} + +TEST_F(SvdUtilsConversionTest, ConvertsReadActions) { + using Action = SvdTypes::ReadAction; + CheckEnumConversion(SvdUtils::ConvertReadAction, { + {"clear", Action::CLEAR}, {"set", Action::SET}, {"modify", Action::MODIFY}, {"modifyExternal", Action::MODIFEXT}, + }); +} + +TEST_F(SvdUtilsConversionTest, MatchesFieldReadWriteCapabilitiesToRegisterAccess) { + using Access = SvdTypes::Access; + const vector> capabilities{ + {Access::UNDEF, 0U}, {Access::READONLY, 1U}, {Access::WRITEONLY, 2U}, {Access::READWRITE, 3U}, + }; + for(const auto& [registerAccess, registerMask] : capabilities) { + for(const auto& [fieldAccess, fieldMask] : capabilities) { + SCOPED_TRACE(testing::Message() << "register=" << static_cast(registerAccess) + << ", field=" << static_cast(fieldAccess)); + const bool expected = fieldMask != 0U && (fieldMask & registerMask) == fieldMask; + EXPECT_EQ(expected, SvdUtils::IsMatchAccess(fieldAccess, registerAccess)); + } + } + EXPECT_TRUE(SvdUtils::IsMatchAccess(Access::WRITEONCE, Access::WRITEONLY)); + EXPECT_TRUE(SvdUtils::IsMatchAccess(Access::WRITEONCE, Access::WRITEONCE)); + EXPECT_TRUE(SvdUtils::IsMatchAccess(Access::READONLY, Access::READWRITEONCE)); + EXPECT_TRUE(SvdUtils::IsMatchAccess(Access::READWRITEONCE, Access::READWRITEONCE)); + EXPECT_FALSE(SvdUtils::IsMatchAccess(Access::READONLY, Access::WRITEONCE)); + EXPECT_FALSE(SvdUtils::IsMatchAccess(Access::WRITEONCE, Access::READONLY)); + EXPECT_FALSE(SvdUtils::IsMatchAccess(Access::UNDEF, Access::WRITEONCE)); + EXPECT_FALSE(SvdUtils::IsMatchAccess(Access::UNDEF, Access::READWRITEONCE)); +} + +TEST_F(SvdUtilsConversionTest, AggregatesFieldAccessWithoutRestrictingOtherFields) { + using Access = SvdTypes::Access; + const Access capabilities[]{Access::UNDEF, Access::READONLY, Access::WRITEONLY, Access::READWRITE}; + for(size_t registerMask = 0; registerMask < 4U; registerMask++) { + for(size_t fieldMask = 0; fieldMask < 4U; fieldMask++) { + SCOPED_TRACE(testing::Message() << "registerMask=" << registerMask << ", fieldMask=" << fieldMask); + const auto expected = capabilities[registerMask | fieldMask]; + EXPECT_EQ(expected, SvdUtils::CalcAccessResult(capabilities[fieldMask], capabilities[registerMask])); + } + } + for(const auto access : {Access::WRITEONCE, Access::READWRITEONCE}) { + EXPECT_EQ(access, SvdUtils::CalcAccessResult(access, Access::UNDEF)); + EXPECT_EQ(access, SvdUtils::CalcAccessResult(Access::UNDEF, access)); + EXPECT_EQ(Access::READWRITE, SvdUtils::CalcAccessResult(access, Access::READWRITE)); + EXPECT_EQ(Access::READWRITE, SvdUtils::CalcAccessResult(Access::READWRITE, access)); + } + EXPECT_EQ(Access::READWRITEONCE, SvdUtils::CalcAccessResult(Access::WRITEONCE, Access::READONLY)); + EXPECT_EQ(Access::READWRITEONCE, SvdUtils::CalcAccessResult(Access::READONLY, Access::WRITEONCE)); +} + +TEST_F(SvdUtilsConversionTest, SortsRegisterNamesNaturallyAndSupportsCaseInsensitiveComparison) { + vector names{"REG10", "REG2", "REG1B", "REG1", "REG1A", "REG"}; + sort(names.begin(), names.end(), SvdUtils::StringAlnumLess{}); + EXPECT_EQ(names, (vector{"REG", "REG1", "REG1A", "REG1B", "REG2", "REG10"})); + EXPECT_EQ(0, SvdUtils::AlnumCmp("REG12A", "reg12a", false)); + EXPECT_LT(SvdUtils::AlnumCmp("REG12A", "reg12a", true), 0); + EXPECT_LT(SvdUtils::AlnumCmp("REG2A", "REG2B"), 0); + EXPECT_GT(SvdUtils::AlnumCmp("REG2B", "REG2A"), 0); + EXPECT_EQ(0, SvdUtils::AlnumCmp(string{}, string{})); + EXPECT_LT(SvdUtils::AlnumCmp(string{}, string{"REG1"}), 0); + EXPECT_GT(SvdUtils::AlnumCmp(string{"REG1"}, string{}), 0); +} + +TEST_F(SvdUtilsConversionTest, ParsesDimensionExpressions) { + struct Case { + const char* text; + SvdTypes::Expression kind; + const char* name; + uint32_t insertPosition; + }; + using Expression = SvdTypes::Expression; + const Case cases[] = { + {"DATA", Expression::NONE, "DATA", numeric_limits::max()}, + {"DATA[%s]", Expression::ARRAY, "DATA", 4U}, + {"CH%s_DATA", Expression::EXTEND, "CH_DATA", 2U}, + }; + for(const auto& test : cases) { + SCOPED_TRACE(test.text); + string name; + uint32_t position = 0U; + EXPECT_EQ(SvdUtils::ParseExpression(test.text, name, position), test.kind); + EXPECT_EQ(name, test.name); + EXPECT_EQ(position, test.insertPosition); + } +} + +TEST_F(SvdUtilsConversionTest, SplitsDerivedNamesAndMarksEmptyComponents) { + const vector>> cases{ + {"PERIPHERAL", {"PERIPHERAL"}}, {"PERIPHERAL.CLUSTER.REG", {"PERIPHERAL", "CLUSTER", "REG"}}, + {"", {"!ERROR!"}}, {".REG", {"!ERROR!", "REG"}}, {"PERIPHERAL.", {"PERIPHERAL", "!ERROR!"}}, + {"PERIPHERAL..REG", {"PERIPHERAL", "!ERROR!", "REG"}}, + }; + for(const auto& [text, expected] : cases) { + SCOPED_TRACE(text); + list names; + EXPECT_TRUE(SvdUtils::ConvertDerivedNameHirachy(text, names)); + EXPECT_EQ(names, expected); + } +} + +} // namespace From de2e7c7faa0dd10d84ade62fc9d027ae616193f5 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Wed, 23 Sep 2026 11:33:29 +0200 Subject: [PATCH 2/2] test(svdconv): check dimension pointers at each use --- .../svdconv/test/unittests/src/SvdModelValidationTest.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp b/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp index 90ad01f95..4dc257eb6 100644 --- a/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp +++ b/tools/svdconv/test/unittests/src/SvdModelValidationTest.cpp @@ -88,7 +88,6 @@ class SvdModelValidationTest : public testing::Test, public IErrConsumer { element.SetText(value); ASSERT_TRUE(reg.ProcessXmlElement(&element)); } - ASSERT_NE(nullptr, reg.GetDimension()); } vector m_messages; @@ -244,6 +243,7 @@ TEST_F(SvdModelValidationTest, ExplicitDimensionIndicesGenerateNames) { SvdRegister reg(nullptr); ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", "LEFT, CENTER, RIGHT")); const auto dim = reg.GetDimension(); + ASSERT_NE(nullptr, dim); ASSERT_TRUE(dim->CalculateDim()); const list expected{"LEFT", "CENTER", "RIGHT"}; @@ -258,6 +258,7 @@ TEST_F(SvdModelValidationTest, AlphabeticDimensionRangeExpandsInOrder) { SvdRegister reg(nullptr); ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", "A-C")); const auto dim = reg.GetDimension(); + ASSERT_NE(nullptr, dim); ASSERT_TRUE(dim->CalculateDim()); const list expected{"A", "B", "C"}; @@ -273,6 +274,7 @@ TEST_F(SvdModelValidationTest, InvalidDimensionRangesDoNotProduceIndices) { SvdRegister reg(nullptr); ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", indices)); const auto dim = reg.GetDimension(); + ASSERT_NE(nullptr, dim); EXPECT_FALSE(dim->CalculateDimIndex()); EXPECT_TRUE(dim->GetDimIndexList().empty()); } @@ -284,7 +286,9 @@ TEST_F(SvdModelValidationTest, DimensionListsDiagnoseCountMismatchAndDuplicateIn m_messages.clear(); SvdRegister reg(nullptr); ASSERT_NO_FATAL_FAILURE(ConfigureDimension(reg, "3", indices)); - ASSERT_TRUE(reg.GetDimension()->CalculateDim()); + const auto dim = reg.GetDimension(); + ASSERT_NE(nullptr, dim); + ASSERT_TRUE(dim->CalculateDim()); EXPECT_TRUE(HasMessage(diagnostic)); } }