From: Razvan Becheriu Date: Tue, 15 Jun 2021 17:31:39 +0000 (+0300) Subject: [#1840] export V4 RAI option and suboptions to run script X-Git-Tag: Kea-1.9.9~94 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=89b72fa34fa27ce8700a4a454d176d9358ade611;p=thirdparty%2Fkea.git [#1840] export V4 RAI option and suboptions to run script --- diff --git a/doc/sphinx/arm/hooks-run-script.rst b/doc/sphinx/arm/hooks-run-script.rst index 7c2111ceec..652a333121 100644 --- a/doc/sphinx/arm/hooks-run-script.rst +++ b/doc/sphinx/arm/hooks-run-script.rst @@ -217,6 +217,9 @@ lease4_renew QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE + QUERY4_RAI + QUERY4_RAI_CIRCUIT_ID + QUERY4_RAI_REMOTE_ID SUBNET4_ID SUBNET4_NAME SUBNET4_PREFIX @@ -289,6 +292,9 @@ leases4_committed QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE + QUERY4_RAI + QUERY4_RAI_CIRCUIT_ID + QUERY4_RAI_REMOTE_ID LEASES4_SIZE DELETED_LEASES4_SIZE @@ -343,6 +349,9 @@ lease4_release QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE + QUERY4_RAI + QUERY4_RAI_CIRCUIT_ID + QUERY4_RAI_REMOTE_ID LEASE4_ADDRESS LEASE4_CLTT LEASE4_HOSTNAME @@ -379,6 +388,9 @@ lease4_decline QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE + QUERY4_RAI + QUERY4_RAI_CIRCUIT_ID + QUERY4_RAI_REMOTE_ID LEASE4_ADDRESS LEASE4_CLTT LEASE4_HOSTNAME diff --git a/src/hooks/dhcp/run_script/run_script.cc b/src/hooks/dhcp/run_script/run_script.cc index fc4e94aac2..8eac5f0bc6 100644 --- a/src/hooks/dhcp/run_script/run_script.cc +++ b/src/hooks/dhcp/run_script/run_script.cc @@ -111,7 +111,6 @@ RunScriptImpl::extractDUID(ProcessEnvVars& vars, const DuidPtr duid, const string& prefix, const string& suffix) { - string data = ""; if (duid) { RunScriptImpl::extractString(vars, duid->toText(), prefix, suffix); @@ -120,6 +119,31 @@ RunScriptImpl::extractDUID(ProcessEnvVars& vars, } } +void +RunScriptImpl::extractOption(ProcessEnvVars& vars, + const OptionPtr option, + const string& prefix, + const string& suffix) { + if (option) { + RunScriptImpl::extractString(vars, option->toHexString(), prefix, suffix); + } else { + RunScriptImpl::extractString(vars, "", prefix, suffix); + } +} + +void +RunScriptImpl::extractSubOption(ProcessEnvVars& vars, + const OptionPtr option, + uint16_t code, + const string& prefix, + const string& suffix) { + if (option) { + RunScriptImpl::extractOption(vars, option->getOption(code), prefix, suffix); + } else { + RunScriptImpl::extractString(vars, "", prefix, suffix); + } +} + void RunScriptImpl::extractOptionIA(ProcessEnvVars& vars, const Option6IAPtr option6IA, @@ -350,6 +374,12 @@ RunScriptImpl::extractPkt4(ProcessEnvVars& vars, prefix + "_LOCAL_HWADDR", suffix); RunScriptImpl::extractHWAddr(vars, pkt4->getRemoteHWAddr(), prefix + "_REMOTE_HWADDR", suffix); + RunScriptImpl::extractOption(vars, pkt4->getOption(82), + prefix + "_RAI", suffix); + RunScriptImpl::extractSubOption(vars, pkt4->getOption(82), 1, + prefix + "_RAI_CIRCUIT_ID", suffix); + RunScriptImpl::extractSubOption(vars, pkt4->getOption(82), 2, + prefix + "_RAI_REMOTE_ID", suffix); } else { RunScriptImpl::extractString(vars, "", prefix + "_TYPE", suffix); RunScriptImpl::extractString(vars, "", prefix + "_TXID", suffix); @@ -373,6 +403,11 @@ RunScriptImpl::extractPkt4(ProcessEnvVars& vars, prefix + "_LOCAL_HWADDR", suffix); RunScriptImpl::extractHWAddr(vars, HWAddrPtr(), prefix + "_REMOTE_HWADDR", suffix); + RunScriptImpl::extractString(vars, "", prefix + "_RAI", suffix); + RunScriptImpl::extractString(vars, "", + prefix + "_RAI_CIRCUIT_ID", suffix); + RunScriptImpl::extractString(vars, "", + prefix + "_RAI_REMOTE_ID", suffix); } } diff --git a/src/hooks/dhcp/run_script/run_script.dox b/src/hooks/dhcp/run_script/run_script.dox index ce29507e83..9398abcbda 100644 --- a/src/hooks/dhcp/run_script/run_script.dox +++ b/src/hooks/dhcp/run_script/run_script.dox @@ -235,6 +235,9 @@ QUERY4_LOCAL_HWADDR QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE +QUERY4_RAI +QUERY4_RAI_CIRCUIT_ID +QUERY4_RAI_REMOTE_ID SUBNET4_ID SUBNET4_NAME SUBNET4_PREFIX @@ -313,6 +316,9 @@ QUERY4_LOCAL_HWADDR QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE +QUERY4_RAI +QUERY4_RAI_CIRCUIT_ID +QUERY4_RAI_REMOTE_ID LEASES4_SIZE DELETED_LEASES4_SIZE @@ -371,6 +377,9 @@ QUERY4_LOCAL_HWADDR QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE +QUERY4_RAI +QUERY4_RAI_CIRCUIT_ID +QUERY4_RAI_REMOTE_ID LEASE4_ADDRESS LEASE4_CLTT LEASE4_HOSTNAME @@ -409,6 +418,9 @@ QUERY4_LOCAL_HWADDR QUERY4_LOCAL_HWADDR_TYPE QUERY4_REMOTE_HWADDR QUERY4_REMOTE_HWADDR_TYPE +QUERY4_RAI +QUERY4_RAI_CIRCUIT_ID +QUERY4_RAI_REMOTE_ID LEASE4_ADDRESS LEASE4_CLTT LEASE4_HOSTNAME diff --git a/src/hooks/dhcp/run_script/run_script.h b/src/hooks/dhcp/run_script/run_script.h index 4fd370c7b1..874c64d212 100644 --- a/src/hooks/dhcp/run_script/run_script.h +++ b/src/hooks/dhcp/run_script/run_script.h @@ -69,7 +69,7 @@ public: /// @brief Extract HWAddr data and append to environment. /// - /// @param value The hwaddr to be exported to target script environment. + /// @param hwaddr The hwaddr to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractHWAddr(isc::asiolink::ProcessEnvVars& vars, @@ -79,7 +79,7 @@ public: /// @brief Extract DUID data and append to environment. /// - /// @param value The duid to be exported to target script environment. + /// @param duid The duid to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractDUID(isc::asiolink::ProcessEnvVars& vars, @@ -87,9 +87,32 @@ public: const std::string& prefix = "", const std::string& suffix = ""); + /// @brief Extract Option data and append to environment. + /// + /// @param option The option to be exported to target script environment. + /// @param prefix The prefix for the name of the environment variable. + /// @param suffix The suffix for the name of the environment variable. + static void extractOption(isc::asiolink::ProcessEnvVars& vars, + const isc::dhcp::OptionPtr option, + const std::string& prefix = "", + const std::string& suffix = ""); + + /// @brief Extract Option SubOption data and append to environment. + /// + /// @param option The parent option of the suboption to be exported to + /// target script environment. + /// @param code The code of the suboption. + /// @param prefix The prefix for the name of the environment variable. + /// @param suffix The suffix for the name of the environment variable. + static void extractSubOption(isc::asiolink::ProcessEnvVars& vars, + const isc::dhcp::OptionPtr option, + uint16_t code, + const std::string& prefix = "", + const std::string& suffix = ""); + /// @brief Extract Option6IA data and append to environment. /// - /// @param value The option6IA to be exported to target script environment. + /// @param option6IA The option6IA to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractOptionIA(isc::asiolink::ProcessEnvVars& vars, @@ -99,7 +122,7 @@ public: /// @brief Extract Subnet4 data and append to environment. /// - /// @param value The subnet4 to be exported to target script environment. + /// @param subnet4 The subnet4 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractSubnet4(isc::asiolink::ProcessEnvVars& vars, @@ -109,7 +132,7 @@ public: /// @brief Extract Subnet6 data and append to environment. /// - /// @param value The subnet6 to be exported to target script environment. + /// @param subnet6 The subnet6 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractSubnet6(isc::asiolink::ProcessEnvVars& vars, @@ -119,7 +142,7 @@ public: /// @brief Extract Lease4 data and append to environment. /// - /// @param value The lease4 to be exported to target script environment. + /// @param lease4 The lease4 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractLease4(isc::asiolink::ProcessEnvVars& vars, @@ -129,7 +152,7 @@ public: /// @brief Extract Lease6 data and append to environment. /// - /// @param value The lease6 to be exported to target script environment. + /// @param lease6 The lease6 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractLease6(isc::asiolink::ProcessEnvVars& vars, @@ -139,7 +162,7 @@ public: /// @brief Extract Lease4Collection data and append to environment. /// - /// @param value The leases4 to be exported to target script environment. + /// @param leases4 The leases4 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractLeases4(isc::asiolink::ProcessEnvVars& vars, @@ -149,7 +172,7 @@ public: /// @brief Extract Lease6Collection data and append to environment. /// - /// @param value The leases6 to be exported to target script environment. + /// @param leases6 The leases6 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractLeases6(isc::asiolink::ProcessEnvVars& vars, @@ -159,7 +182,7 @@ public: /// @brief Extract Pkt4 data and append to environment. /// - /// @param value The pkt4 to be exported to target script environment. + /// @param pkt4 The pkt4 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractPkt4(isc::asiolink::ProcessEnvVars& vars, @@ -169,7 +192,7 @@ public: /// @brief Extract Pkt6 data and append to environment. /// - /// @param value The pkt6 to be exported to target script environment. + /// @param pkt6 The pkt6 to be exported to target script environment. /// @param prefix The prefix for the name of the environment variable. /// @param suffix The suffix for the name of the environment variable. static void extractPkt6(isc::asiolink::ProcessEnvVars& vars, diff --git a/src/hooks/dhcp/run_script/tests/run_script_test.sh.in b/src/hooks/dhcp/run_script/tests/run_script_test.sh.in index 68b145655e..9ac05902ed 100644 --- a/src/hooks/dhcp/run_script/tests/run_script_test.sh.in +++ b/src/hooks/dhcp/run_script/tests/run_script_test.sh.in @@ -64,6 +64,9 @@ lease4_renew () { TEST_EQ "QUERY4_LOCAL_HWADDR_TYPE" "1" TEST_EQ "QUERY4_REMOTE_HWADDR" "00:01:02:03" TEST_EQ "QUERY4_REMOTE_HWADDR_TYPE" "1" + TEST_EQ "QUERY4_RAI" "0x0105686F776479020587F67977EF06061A2B3C4D5E6F" + TEST_EQ "QUERY4_RAI_CIRCUIT_ID" "0x686F776479" + TEST_EQ "QUERY4_RAI_REMOTE_ID" "0x87F67977EF" TEST_EQ "SUBNET4_ID" "6" TEST_EQ "SUBNET4_NAME" "182.168.0.1/2" TEST_EQ "SUBNET4_PREFIX" "182.168.0.1" @@ -133,6 +136,9 @@ leases4_committed () { TEST_EQ "QUERY4_LOCAL_HWADDR_TYPE" "1" TEST_EQ "QUERY4_REMOTE_HWADDR" "00:01:02:03" TEST_EQ "QUERY4_REMOTE_HWADDR_TYPE" "1" + TEST_EQ "QUERY4_RAI" "0x0105686F776479020587F67977EF06061A2B3C4D5E6F" + TEST_EQ "QUERY4_RAI_CIRCUIT_ID" "0x686F776479" + TEST_EQ "QUERY4_RAI_REMOTE_ID" "0x87F67977EF" TEST_EQ "LEASES4_SIZE" "2" TEST_EQ "LEASES4_AT0_ADDRESS" "" TEST_EQ "LEASES4_AT0_CLTT" "" @@ -197,6 +203,9 @@ lease4_release () { TEST_EQ "QUERY4_LOCAL_HWADDR_TYPE" "1" TEST_EQ "QUERY4_REMOTE_HWADDR" "00:01:02:03" TEST_EQ "QUERY4_REMOTE_HWADDR_TYPE" "1" + TEST_EQ "QUERY4_RAI" "0x0105686F776479020587F67977EF06061A2B3C4D5E6F" + TEST_EQ "QUERY4_RAI_CIRCUIT_ID" "0x686F776479" + TEST_EQ "QUERY4_RAI_REMOTE_ID" "0x87F67977EF" TEST_EQ "LEASE4_ADDRESS" "192.168.0.1" TEST_EQ "LEASE4_CLTT" "3" TEST_EQ "LEASE4_HOSTNAME" "test.hostname" @@ -232,6 +241,9 @@ lease4_decline () { TEST_EQ "QUERY4_LOCAL_HWADDR_TYPE" "1" TEST_EQ "QUERY4_REMOTE_HWADDR" "00:01:02:03" TEST_EQ "QUERY4_REMOTE_HWADDR_TYPE" "1" + TEST_EQ "QUERY4_RAI" "0x0105686F776479020587F67977EF06061A2B3C4D5E6F" + TEST_EQ "QUERY4_RAI_CIRCUIT_ID" "0x686F776479" + TEST_EQ "QUERY4_RAI_REMOTE_ID" "0x87F67977EF" TEST_EQ "LEASE4_ADDRESS" "192.168.0.1" TEST_EQ "LEASE4_CLTT" "3" TEST_EQ "LEASE4_HOSTNAME" "test.hostname" diff --git a/src/hooks/dhcp/run_script/tests/run_script_unittests.cc b/src/hooks/dhcp/run_script/tests/run_script_unittests.cc index 06e1daf071..fb4a35466b 100644 --- a/src/hooks/dhcp/run_script/tests/run_script_unittests.cc +++ b/src/hooks/dhcp/run_script/tests/run_script_unittests.cc @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -86,6 +89,23 @@ generateDUID() { return (ClientIdPtr(new ClientId({0, 1, 2, 3, 4, 5, 6}))); } +/// @brief Generate a valid Option. +/// +/// @param universe The option universe (V4 or V6). +/// @param code The Option code to use. +/// @return The generated Option. +OptionPtr +generateOption(Option::Universe universe, uint16_t code, OptionBuffer& data) { + OptionDefinitionPtr def = LibDHCP::getOptionDef(universe == Option::V4 ? + DHCP4_OPTION_SPACE : DHCP6_OPTION_SPACE, + code); + if (def) { + return (OptionCustomPtr(new OptionCustom(*def, universe, data))); + } + + return (OptionPtr()); +} + /// @brief Generate a valid Option6IA. /// /// @return The generated Option6IA. @@ -219,6 +239,31 @@ generatePkt4() { pkt4->setLocalHWAddr(generateHWAddr()); pkt4->setRemoteHWAddr(generateHWAddr()); + OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE, + DHO_DHCP_AGENT_OPTIONS); + + OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4)); + + uint8_t circuit_id[] = { 0x68, 0x6F, 0x77, 0x64, 0x79 }; + OptionPtr circuit_id_opt(new Option(Option::V4, RAI_OPTION_AGENT_CIRCUIT_ID, + OptionBuffer(circuit_id, + circuit_id + sizeof(circuit_id)))); + rai->addOption(circuit_id_opt); + + uint8_t subscriber_id[] = { 0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f }; + OptionPtr subscriber_id_opt(new Option(Option::V4, RAI_OPTION_SUBSCRIBER_ID, + OptionBuffer(subscriber_id, + subscriber_id + sizeof(subscriber_id)))); + rai->addOption(subscriber_id_opt); + + uint8_t remote_id[] = { 0x87, 0xF6, 0x79, 0x77, 0xEF }; + OptionPtr remote_id_opt(new Option(Option::V4, RAI_OPTION_REMOTE_ID, + OptionBuffer(remote_id, + remote_id + sizeof(remote_id)))); + rai->addOption(remote_id_opt); + + pkt4->addOption(rai); + return (pkt4); } @@ -379,6 +424,45 @@ TEST(RunScript, extractDUID) { EXPECT_EQ(expected, join(vars)); } +/// @brief Tests the extractOption method works as expected. +TEST(RunScript, extractOption) { + ProcessEnvVars vars; + OptionPtr option; + RunScriptImpl::extractOption(vars, option, "OPTION_PREFIX", "_OPTION_SUFFIX"); + ASSERT_EQ(1, vars.size()); + string expected = "OPTION_PREFIX_OPTION_SUFFIX=\n"; + EXPECT_EQ(expected, join(vars)); + vars.clear(); + OptionBuffer buffer = { 0xca, 0xfe, 0xba, 0xbe }; + option = generateOption(Option::V4, DHO_USER_CLASS, buffer); + RunScriptImpl::extractOption(vars, option, "OPTION_PREFIX", "_OPTION_SUFFIX"); + ASSERT_EQ(1, vars.size()); + expected = "OPTION_PREFIX_OPTION_SUFFIX=0xCAFEBABE\n"; + EXPECT_EQ(expected, join(vars)); +} + +/// @brief Tests the extractSubOption method works as expected. +TEST(RunScript, extractSubOption) { + ProcessEnvVars vars; + OptionPtr option; + RunScriptImpl::extractOption(vars, option, "OPTION_SUBOPTION_PREFIX", "_OPTION_SUBOPTION_SUFFIX"); + ASSERT_EQ(1, vars.size()); + string expected = "OPTION_SUBOPTION_PREFIX_OPTION_SUBOPTION_SUFFIX=\n"; + EXPECT_EQ(expected, join(vars)); + vars.clear(); + OptionBuffer data; + option = generateOption(Option::V4, DHO_DHCP_AGENT_OPTIONS, data); + uint8_t subscriber_id[] = { 0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f }; + OptionPtr subscriber_id_opt(new Option(Option::V4, RAI_OPTION_SUBSCRIBER_ID, + OptionBuffer(subscriber_id, + subscriber_id + sizeof(subscriber_id)))); + option->addOption(subscriber_id_opt); + RunScriptImpl::extractSubOption(vars, option, RAI_OPTION_SUBSCRIBER_ID, "OPTION_SUBOPTION_PREFIX", "_OPTION_SUBOPTION_SUFFIX"); + ASSERT_EQ(1, vars.size()); + expected = "OPTION_SUBOPTION_PREFIX_OPTION_SUBOPTION_SUFFIX=0x1A2B3C4D5E6F\n"; + EXPECT_EQ(expected, join(vars)); +} + /// @brief Tests the extractOptionIA method works as expected. TEST(RunScript, extractOptionIA) { ProcessEnvVars vars; @@ -602,7 +686,7 @@ TEST(RunScript, extractPkt4) { ProcessEnvVars vars; Pkt4Ptr pkt4; RunScriptImpl::extractPkt4(vars, pkt4, "PKT4_PREFIX", "_PKT4_SUFFIX"); - ASSERT_EQ(22, vars.size()); + ASSERT_EQ(25, vars.size()); string expected = "PKT4_PREFIX_TYPE_PKT4_SUFFIX=\n" "PKT4_PREFIX_TXID_PKT4_SUFFIX=\n" "PKT4_PREFIX_LOCAL_ADDR_PKT4_SUFFIX=\n" @@ -624,12 +708,15 @@ TEST(RunScript, extractPkt4) { "PKT4_PREFIX_LOCAL_HWADDR_PKT4_SUFFIX=\n" "PKT4_PREFIX_LOCAL_HWADDR_TYPE_PKT4_SUFFIX=\n" "PKT4_PREFIX_REMOTE_HWADDR_PKT4_SUFFIX=\n" - "PKT4_PREFIX_REMOTE_HWADDR_TYPE_PKT4_SUFFIX=\n"; + "PKT4_PREFIX_REMOTE_HWADDR_TYPE_PKT4_SUFFIX=\n" + "PKT4_PREFIX_RAI_PKT4_SUFFIX=\n" + "PKT4_PREFIX_RAI_CIRCUIT_ID_PKT4_SUFFIX=\n" + "PKT4_PREFIX_RAI_REMOTE_ID_PKT4_SUFFIX=\n"; EXPECT_EQ(expected, join(vars)); vars.clear(); pkt4 = generatePkt4(); RunScriptImpl::extractPkt4(vars, pkt4, "PKT4_PREFIX", "_PKT4_SUFFIX"); - ASSERT_EQ(22, vars.size()); + ASSERT_EQ(25, vars.size()); expected = "PKT4_PREFIX_TYPE_PKT4_SUFFIX=UNKNOWN\n" "PKT4_PREFIX_TXID_PKT4_SUFFIX=0\n" "PKT4_PREFIX_LOCAL_ADDR_PKT4_SUFFIX=0.0.0.0\n" @@ -651,7 +738,10 @@ TEST(RunScript, extractPkt4) { "PKT4_PREFIX_LOCAL_HWADDR_PKT4_SUFFIX=00:01:02:03\n" "PKT4_PREFIX_LOCAL_HWADDR_TYPE_PKT4_SUFFIX=1\n" "PKT4_PREFIX_REMOTE_HWADDR_PKT4_SUFFIX=00:01:02:03\n" - "PKT4_PREFIX_REMOTE_HWADDR_TYPE_PKT4_SUFFIX=1\n"; + "PKT4_PREFIX_REMOTE_HWADDR_TYPE_PKT4_SUFFIX=1\n" + "PKT4_PREFIX_RAI_PKT4_SUFFIX=0x0105686F776479020587F67977EF06061A2B3C4D5E6F\n" + "PKT4_PREFIX_RAI_CIRCUIT_ID_PKT4_SUFFIX=0x686F776479\n" + "PKT4_PREFIX_RAI_REMOTE_ID_PKT4_SUFFIX=0x87F67977EF\n"; EXPECT_EQ(expected, join(vars)); }