diff options
Diffstat (limited to 'test/network_tests/magic_cookies_tests')
7 files changed, 664 insertions, 0 deletions
diff --git a/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_client.json.in b/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_client.json.in new file mode 100644 index 0000000..bcb2e29 --- /dev/null +++ b/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_client.json.in @@ -0,0 +1,85 @@ +{ + "unicast":"@TEST_IP_SLAVE@", + "netmask":"255.255.255.0", + "logging": + { + "level":"info", + "console":"true", + "file": + { + "enable":"true", + "path":"/var/log/vsomeip.log" + }, + "dlt":"true" + }, + "applications": + [ + { + "name":"magic_cookies_test_client", + "id":"0x1343" + } + ], + "services": + [ + { + "service":"0x1234", + "instance":"0x5678", + "unicast":"@TEST_IP_MASTER@", + "reliable": + { + "port":"30509", + "enable-magic-cookies":"true" + }, + "events": + [ + { + "event":"0x0777", + "is_field":"true" + }, + { + "event":"0x0778", + "is_field":"false" + }, + { + "event":"0x0779", + "is_field":"true" + } + ], + "eventgroups": + [ + { + "eventgroup":"0x4455", + "events": + [ + "0x777", + "0x778" + ] + }, + { + "eventgroup":"0x4465", + "events": + [ + "0x778", + "0x779" + ] + }, + { + "eventgroup":"0x4555", + "events": + [ + "0x777", + "0x779" + ] + } + ] + } + ], + "routing":"magic_cookies_test_client", + "service-discovery": + { + "enable":"false", + "multicast":"224.0.0.1", + "port":"30490", + "protocol":"udp" + } +} diff --git a/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_service.json.in b/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_service.json.in new file mode 100644 index 0000000..0e53c24 --- /dev/null +++ b/test/network_tests/magic_cookies_tests/conf/magic_cookies_test_service.json.in @@ -0,0 +1,88 @@ +{ + "unicast":"@TEST_IP_MASTER@", + "logging": + { + "level":"debug", + "console":"true", + "file": + { + "enable":"false", + "path":"/tmp/vsomeip.log" + }, + "dlt":"false" + }, + "applications": + [ + { + "name":"magic_cookies_test_service", + "id":"0x1277" + } + ], + "services": + [ + { + "service":"0x1234", + "instance":"0x5678", + "reliable": + { + "port":"30509", + "enable-magic-cookies":"true" + }, + "events": + [ + { + "event":"0x0777", + "is_field":"false", + "is_reliable":"true", + "update-cycle":"2000" + }, + { + "event":"0x0778", + "is_field":"true", + "is_reliable":"true", + "update-cycle":"0" + }, + { + "event":"0x0779", + "is_field":"false", + "is_reliable":"true" + } + ], + "eventgroups": + [ + { + "eventgroup":"0x4455", + "events": + [ + "0x777", + "0x778" + ] + }, + { + "eventgroup":"0x4465", + "events": + [ + "0x778", + "0x779" + ] + }, + { + "eventgroup":"0x4555", + "events": + [ + "0x777", + "0x779" + ] + } + ] + } + ], + "routing":"magic_cookies_test_service", + "service-discovery": + { + "enable":"false", + "multicast":"224.0.0.1", + "port":"30490", + "protocol":"udp" + } +} diff --git a/test/network_tests/magic_cookies_tests/magic_cookies_test_client.cpp b/test/network_tests/magic_cookies_tests/magic_cookies_test_client.cpp new file mode 100644 index 0000000..89a0029 --- /dev/null +++ b/test/network_tests/magic_cookies_tests/magic_cookies_test_client.cpp @@ -0,0 +1,246 @@ +// Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <chrono> +#include <condition_variable> +#include <iomanip> +#include <memory> +#include <thread> + +#include <gtest/gtest.h> + +#include <vsomeip/vsomeip.hpp> + +#include "../someip_test_globals.hpp" +#include "../implementation/runtime/include/application_impl.hpp" +#include "../implementation/routing/include/routing_manager.hpp" + +class magic_cookies_test_client { +public: + magic_cookies_test_client() + : app_(new vsomeip::application_impl("", "")), + is_blocked_(false), + sent_messages_good_(8), + sent_messages_bad_(7), + received_responses_(0), + received_errors_(0), + wait_for_replies_(true), + runner_(std::bind(&magic_cookies_test_client::run, this)) { + } + + void init() { + VSOMEIP_INFO << "Initializing..."; + if (!app_->init()) { + ADD_FAILURE() << "Couldn't initialize application"; + exit(EXIT_FAILURE); + } + + app_->register_state_handler( + std::bind( + &magic_cookies_test_client::on_state, + this, + std::placeholders::_1)); + + app_->register_message_handler( + vsomeip::ANY_SERVICE, vsomeip_test::TEST_SERVICE_INSTANCE_ID, vsomeip::ANY_METHOD, + std::bind(&magic_cookies_test_client::on_message, + this, + std::placeholders::_1)); + + app_->register_availability_handler(vsomeip_test::TEST_SERVICE_SERVICE_ID, vsomeip_test::TEST_SERVICE_INSTANCE_ID, + std::bind(&magic_cookies_test_client::on_availability, + this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + vsomeip::DEFAULT_MAJOR, vsomeip::DEFAULT_MINOR); + } + + void start() { + VSOMEIP_INFO << "Starting..."; + app_->start(); + } + + void stop() { + VSOMEIP_INFO << "Stopping..."; + app_->clear_all_handler(); + app_->stop(); + } + + void on_state(vsomeip::state_type_e _state) { + if (_state == vsomeip::state_type_e::ST_REGISTERED) { + VSOMEIP_INFO << "Client registration done."; + app_->request_service(vsomeip_test::TEST_SERVICE_SERVICE_ID, + vsomeip_test::TEST_SERVICE_INSTANCE_ID, + vsomeip::ANY_MAJOR, vsomeip::ANY_MINOR); + } + } + + void on_availability(vsomeip::service_t _service, vsomeip::instance_t _instance, bool _is_available) { + VSOMEIP_INFO << "Service [" + << std::setw(4) << std::setfill('0') << std::hex << _service << "." << _instance + << "] is " + << (_is_available ? "available." : "NOT available."); + + if (vsomeip_test::TEST_SERVICE_SERVICE_ID == _service && vsomeip_test::TEST_SERVICE_INSTANCE_ID == _instance) { + static bool is_available = false; + if (is_available && !_is_available) is_available = false; + else if (_is_available && !is_available) { + is_available = true; + std::lock_guard< std::mutex > its_lock(mutex_); + is_blocked_ = true; + condition_.notify_one(); + } + } + } + + void on_message(const std::shared_ptr< vsomeip::message > &_response) { + if (_response->get_return_code() == vsomeip::return_code_e::E_OK) { + VSOMEIP_INFO << "Received a response from Service [" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_service() + << "." + << std::setw(4) << std::setfill('0') << std::hex << _response->get_instance() + << "] to Client/Session [" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_client() + << "/" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_session() + << "]"; + received_responses_++; + } else if (_response->get_return_code() == vsomeip::return_code_e::E_MALFORMED_MESSAGE) { + VSOMEIP_INFO << "Received an error message from Service [" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_service() + << "." + << std::setw(4) << std::setfill('0') << std::hex << _response->get_instance() + << "] to Client/Session [" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_client() + << "/" + << std::setw(4) << std::setfill('0') << std::hex << _response->get_session() + << "]"; + received_errors_++; + } + if (received_errors_ == sent_messages_bad_ + && received_responses_ == sent_messages_good_) { + std::lock_guard<std::mutex> its_lock(mutex_); + wait_for_replies_ = false; + condition_.notify_one(); + } + } + + void join() { + runner_.join(); + } + + void run() { + std::unique_lock< std::mutex > its_lock(mutex_); + while (!is_blocked_) { + if (std::cv_status::timeout == + condition_.wait_for(its_lock, std::chrono::milliseconds(5000))) { + GTEST_NONFATAL_FAILURE_("Service didn't become available within 5s."); + break; + } + } + VSOMEIP_INFO << "Running..."; + + vsomeip::routing_manager *its_routing = app_->get_routing_manager(); + + vsomeip::byte_t its_good_payload_data[] = { + 0x12, 0x34, 0x84, 0x21, + 0x00, 0x00, 0x00, 0x11, + 0x13, 0x43, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01 + }; + + vsomeip::byte_t its_bad_payload_data[] = { + 0x12, 0x34, 0x84, 0x21, + 0x00, 0x00, 0x01, 0x23, + 0x13, 0x43, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01 + }; + + // Test sequence + its_good_payload_data[11] = 0x01; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x02; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x03; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x04; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x05; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x06; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x07; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x08; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x09; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x0A; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x0B; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x0C; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x0D; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_bad_payload_data[11] = 0x0E; + its_routing->send(0x1343, its_bad_payload_data, sizeof(its_bad_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + std::this_thread::sleep_for(std::chrono::seconds(11)); + its_good_payload_data[11] = 0x0F; + its_routing->send(0x1343, its_good_payload_data, sizeof(its_good_payload_data), vsomeip_test::TEST_SERVICE_INSTANCE_ID, true); + + while (wait_for_replies_) { + if(std::cv_status::timeout == + condition_.wait_for(its_lock, std::chrono::milliseconds(5000))) { + GTEST_NONFATAL_FAILURE_("Didn't receive all replies/errors in time"); + break; + } + } + EXPECT_EQ(sent_messages_good_, received_responses_); + EXPECT_EQ(sent_messages_bad_, received_errors_); + stop(); + } + +private: + std::shared_ptr< vsomeip::application_impl > app_; + std::mutex mutex_; + std::condition_variable condition_; + bool is_blocked_; + const std::uint32_t sent_messages_good_; + const std::uint32_t sent_messages_bad_; + std::atomic<std::uint32_t> received_responses_; + std::atomic<std::uint32_t> received_errors_; + bool wait_for_replies_; + std::thread runner_; +}; + +TEST(someip_magic_cookies_test, send_good_and_bad_messages) +{ + magic_cookies_test_client its_client; + its_client.init(); + its_client.start(); + its_client.join(); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + + diff --git a/test/network_tests/magic_cookies_tests/magic_cookies_test_client_start.sh b/test/network_tests/magic_cookies_tests/magic_cookies_test_client_start.sh new file mode 100755 index 0000000..45f4d3c --- /dev/null +++ b/test/network_tests/magic_cookies_tests/magic_cookies_test_client_start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +export VSOMEIP_APPLICATION_NAME=magic_cookies_test_client +export VSOMEIP_CONFIGURATION=magic_cookies_test_client.json +./magic_cookies_test_client diff --git a/test/network_tests/magic_cookies_tests/magic_cookies_test_service.cpp b/test/network_tests/magic_cookies_tests/magic_cookies_test_service.cpp new file mode 100644 index 0000000..d8ac4d7 --- /dev/null +++ b/test/network_tests/magic_cookies_tests/magic_cookies_test_service.cpp @@ -0,0 +1,168 @@ +// Copyright (C) 2014-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <chrono> +#include <condition_variable> +#include <iomanip> +#include <iostream> +#include <sstream> +#include <thread> + +#include <gtest/gtest.h> + +#include <vsomeip/vsomeip.hpp> + +#include "../someip_test_globals.hpp" + +class magic_cookies_test_service { +public: + magic_cookies_test_service(bool _use_static_routing) : + app_(vsomeip::runtime::get()->create_application()), + is_registered_(false), + use_static_routing_(_use_static_routing), + blocked_(false), + offer_thread_(std::bind(&magic_cookies_test_service::run, this)) { + } + + ~magic_cookies_test_service() { + offer_thread_.join(); + } + void init() { + std::lock_guard<std::mutex> its_lock(mutex_); + + if (!app_->init()) { + ADD_FAILURE() << "Couldn't initialize application"; + exit(EXIT_FAILURE); + } + app_->register_message_handler( + vsomeip_test::TEST_SERVICE_SERVICE_ID, + vsomeip_test::TEST_SERVICE_INSTANCE_ID, + vsomeip_test::TEST_SERVICE_METHOD_ID, + std::bind(&magic_cookies_test_service::on_message, this, + std::placeholders::_1)); + + app_->register_state_handler( + std::bind(&magic_cookies_test_service::on_state, this, + std::placeholders::_1)); + + VSOMEIP_INFO<< "Static routing " << (use_static_routing_ ? "ON" : "OFF"); + } + + void start() { + app_->start(); + } + + void offer() { + app_->offer_service(vsomeip_test::TEST_SERVICE_SERVICE_ID, vsomeip_test::TEST_SERVICE_INSTANCE_ID); + } + + void stop_offer() { + app_->stop_offer_service(vsomeip_test::TEST_SERVICE_SERVICE_ID, vsomeip_test::TEST_SERVICE_INSTANCE_ID); + } + + void on_state(vsomeip::state_type_e _state) { + VSOMEIP_INFO << "Application " << app_->get_name() << " is " + << (_state == vsomeip::state_type_e::ST_REGISTERED ? + "registered." : "deregistered."); + + if (_state == vsomeip::state_type_e::ST_REGISTERED) { + if (!is_registered_) { + is_registered_ = true; + std::lock_guard<std::mutex> its_lock(mutex_); + blocked_ = true; + condition_.notify_one(); + } + } else { + is_registered_ = false; + } + } + + void on_message(const std::shared_ptr<vsomeip::message> &_request) { + VSOMEIP_INFO << "Received a message with Client/Session [" << std::setw(4) + << std::setfill('0') << std::hex << _request->get_client() << "/" + << std::setw(4) << std::setfill('0') << std::hex + << _request->get_session() << "]"; + + std::shared_ptr<vsomeip::message> its_response = vsomeip::runtime::get() + ->create_response(_request); + + std::shared_ptr<vsomeip::payload> its_payload = vsomeip::runtime::get() + ->create_payload(); + std::vector<vsomeip::byte_t> its_payload_data; + for (std::size_t i = 0; i < 120; ++i) + its_payload_data.push_back(static_cast<vsomeip::byte_t>(i % 256)); + its_payload->set_data(its_payload_data); + its_response->set_payload(its_payload); + + app_->send(its_response); + if(_request->get_session() == 0x0F) { + std::lock_guard<std::mutex> its_lock(mutex_); + blocked_ = true; + condition_.notify_one(); + } + } + + void run() { + std::unique_lock<std::mutex> its_lock(mutex_); + while (!blocked_) + condition_.wait(its_lock); + + bool is_offer(true); + blocked_ = false; + + if (use_static_routing_) { + offer(); + while (!blocked_) { + if(std::cv_status::timeout == + condition_.wait_for(its_lock, std::chrono::seconds(200))) { + GTEST_NONFATAL_FAILURE_("Didn't receive all requests within time"); + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + app_->clear_all_handler(); + app_->stop(); + } else { + while (true) { + if (is_offer) + offer(); + else + stop_offer(); + std::this_thread::sleep_for(std::chrono::milliseconds(10000)); + is_offer = !is_offer; + } + } + } + +private: + std::shared_ptr<vsomeip::application> app_; + bool is_registered_; + bool use_static_routing_; + + std::mutex mutex_; + std::condition_variable condition_; + bool blocked_; + std::thread offer_thread_; +}; + +static bool use_static_routing = false; + +TEST(someip_magic_cookies_test, reply_to_good_messages) +{ + magic_cookies_test_service its_sample(use_static_routing); + its_sample.init(); + its_sample.start(); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + std::string static_routing_enable("--static-routing"); + for (int i = 1; i < argc; i++) { + if (static_routing_enable == argv[i]) { + use_static_routing = true; + } + } + return RUN_ALL_TESTS(); +} diff --git a/test/network_tests/magic_cookies_tests/magic_cookies_test_service_start.sh b/test/network_tests/magic_cookies_tests/magic_cookies_test_service_start.sh new file mode 100755 index 0000000..9288b7e --- /dev/null +++ b/test/network_tests/magic_cookies_tests/magic_cookies_test_service_start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +export VSOMEIP_APPLICATION_NAME=magic_cookies_test_service +export VSOMEIP_CONFIGURATION=magic_cookies_test_service.json +./magic_cookies_test_service --tcp --static-routing diff --git a/test/network_tests/magic_cookies_tests/magic_cookies_test_starter.sh b/test/network_tests/magic_cookies_tests/magic_cookies_test_starter.sh new file mode 100755 index 0000000..1e9cfc4 --- /dev/null +++ b/test/network_tests/magic_cookies_tests/magic_cookies_test_starter.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Copyright (C) 2015-2017 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Purpose: This script is needed to start the client and service with +# one command. This is necessary as ctest - which is used to run the +# tests - isn't able to start two binaries for one testcase. Therefore +# the testcase simply executes this script. This script then runs client +# and service and checks that both exit successfully. + +# Display a message to show the user that he must now call the external service +# to finish the test successfully + +FAIL=0 + +if [ ! -z "$USE_LXC_TEST" ]; then + echo "starting magic cookies test on slave LXC" + ssh -tt -i $SANDBOX_ROOT_DIR/commonapi_main/lxc-config/.ssh/mgc_lxc/rsa_key_file.pub -o StrictHostKeyChecking=no root@$LXC_TEST_SLAVE_IP "bash -ci \"set -m; cd \\\$SANDBOX_TARGET_DIR/vsomeip_lib/test/network_tests; ./magic_cookies_test_client_start.sh\"" & +elif [ ! -z "$USE_DOCKER" ]; then + docker exec $DOCKER_IMAGE sh -c "cd $DOCKER_TESTS && ./magic_cookies_test_client_start.sh" & +else +cat <<End-of-message +******************************************************************************* +******************************************************************************* +** Please now run: +** magic_cookies_test_client_start.sh +** from an external host to successfully complete this test. +** +** You probably will need to adapt the 'unicast' settings in +** magic_cookies_client.json and +** magic_cookies_service.json to your personal setup. +** +******************************************************************************* +******************************************************************************* +End-of-message +fi + +# Start the client for magic-cookies test +export VSOMEIP_APPLICATION_NAME=magic_cookies_test_service +export VSOMEIP_CONFIGURATION=magic_cookies_test_service.json +./magic_cookies_test_service --tcp --static-routing & + +# Wait until client and service are finished +for job in $(jobs -p) +do + # Fail gets incremented if either client or service exit + # with a non-zero exit code + wait $job || ((FAIL+=1)) +done + +# Check if client and server both exited successfully +if [ $FAIL -eq 0 ] +then + exit 0 +else + exit 1 +fi |