From patchwork Tue Aug 2 16:08:24 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Maxim Uvarov X-Patchwork-Id: 73168 Delivered-To: patch@linaro.org Received: by 10.140.29.52 with SMTP id a49csp246097qga; Tue, 2 Aug 2016 09:08:58 -0700 (PDT) X-Received: by 10.129.116.86 with SMTP id p83mr47660198ywc.151.1470154137677; Tue, 02 Aug 2016 09:08:57 -0700 (PDT) Return-Path: Received: from lists.linaro.org (lists.linaro.org. [54.225.227.206]) by mx.google.com with ESMTP id 29si1933366qtr.34.2016.08.02.09.08.56; Tue, 02 Aug 2016 09:08:57 -0700 (PDT) Received-SPF: pass (google.com: domain of lng-odp-bounces@lists.linaro.org designates 54.225.227.206 as permitted sender) client-ip=54.225.227.206; Authentication-Results: mx.google.com; spf=pass (google.com: domain of lng-odp-bounces@lists.linaro.org designates 54.225.227.206 as permitted sender) smtp.mailfrom=lng-odp-bounces@lists.linaro.org; dmarc=pass (p=NONE dis=NONE) header.from=linaro.org Received: by lists.linaro.org (Postfix, from userid 109) id D89FC685C8; Tue, 2 Aug 2016 16:08:56 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on ip-10-142-244-252 X-Spam-Level: X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00,URIBL_BLOCKED autolearn=disabled version=3.4.0 Received: from [127.0.0.1] (localhost [127.0.0.1]) by lists.linaro.org (Postfix) with ESMTP id 9D88F68032; Tue, 2 Aug 2016 16:08:41 +0000 (UTC) X-Original-To: lng-odp@lists.linaro.org Delivered-To: lng-odp@lists.linaro.org Received: by lists.linaro.org (Postfix, from userid 109) id 03A6D68043; Tue, 2 Aug 2016 16:08:33 +0000 (UTC) Received: from mail-lf0-f53.google.com (mail-lf0-f53.google.com [209.85.215.53]) by lists.linaro.org (Postfix) with ESMTPS id EB0CF6802F for ; Tue, 2 Aug 2016 16:08:31 +0000 (UTC) Received: by mail-lf0-f53.google.com with SMTP id b199so142110242lfe.0 for ; Tue, 02 Aug 2016 09:08:31 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:from:to:cc:subject:date:message-id; bh=6fFfIHFbT3ZlUDDyb983CozCJphdhhGT03NwDJdlGds=; b=mRzKRbTTTH98qMcPGa+WSE1QzrEwW1PD7H8f1l53VGumkQm+SHJcoF9JnMw6M9FiaB dXA6wzYTJsMwMWebRV6B+T9+eT0H7A+bNT3lRET3dqe5pcOJMwhgldaOWqABw75JESsO T8T79YL14m8UiKkTDM5MBF0/rMCPd9G4pU8G2/tT0ayTPysXaRNu/HUVB3FzrVIU70D6 qlfOSdhdjZkObD3DQpieVRFhnGmCHrrga6offL0fEs9/xnSYU5UECy5PBNxZhv/e5TUm +dllX0RmaPBNC2XBjnA0FtMcKvHARhFdd1i59tZKnE0vGkwkqR54oXj9y9Epu6adeF/p 9LgA== X-Gm-Message-State: AEkoouuBHyNixU+Vy9DmNjedZ7QGSCQqtS1RYdZ1OY0cSL70aom3UGm4i7l8NU72LI0Q6XNpx+w= X-Received: by 10.25.20.226 with SMTP id 95mr17726920lfu.194.1470154110354; Tue, 02 Aug 2016 09:08:30 -0700 (PDT) Received: from localhost.localdomain ([83.220.239.225]) by smtp.gmail.com with ESMTPSA id p102sm605242lfi.9.2016.08.02.09.08.28 (version=TLS1_2 cipher=ECDHE-RSA-AES128-SHA bits=128/128); Tue, 02 Aug 2016 09:08:29 -0700 (PDT) From: Maxim Uvarov To: lng-odp@lists.linaro.org Date: Tue, 2 Aug 2016 19:08:24 +0300 Message-Id: <1470154104-23566-1-git-send-email-maxim.uvarov@linaro.org> X-Mailer: git-send-email 2.7.1.250.gff4ea60 X-Topics: patch Subject: [lng-odp] [PATCHv2] test: linux-gen: add pcap playback test X-BeenThere: lng-odp@lists.linaro.org X-Mailman-Version: 2.1.16 Precedence: list List-Id: "The OpenDataPlane \(ODP\) List" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: lng-odp-bounces@lists.linaro.org Sender: "lng-odp" add pcap play back test which takes 2 arguments: 1 - pcap file, 2 - packet mask to match. Intend is to test odp with different input traffic to check internal implementation functions. In current case it's test for vlan tag instertion for packet mmap: pkt_mmap_vlan_insert(). Signed-off-by: Maxim Uvarov --- v2: up the patch, dirrect execution looks like works. test/linux-generic/.gitignore | 1 + test/linux-generic/Makefile.am | 10 +- test/linux-generic/cls/.gitignore | 3 + test/linux-generic/cls/Makefile.am | 13 ++ test/linux-generic/cls/cls.c | 349 ++++++++++++++++++++++++++++++++++++ test/linux-generic/cls/cls_main.c | 12 ++ test/linux-generic/cls/cls_suites.h | 1 + test/linux-generic/cls/vlan.pcap | Bin 0 -> 9728 bytes test/linux-generic/cls/vlan_run.sh | 49 +++++ test/linux-generic/m4/configure.m4 | 1 + 10 files changed, 435 insertions(+), 4 deletions(-) create mode 100644 test/linux-generic/cls/.gitignore create mode 100644 test/linux-generic/cls/Makefile.am create mode 100644 test/linux-generic/cls/cls.c create mode 100644 test/linux-generic/cls/cls_main.c create mode 100644 test/linux-generic/cls/cls_suites.h create mode 100644 test/linux-generic/cls/vlan.pcap create mode 100755 test/linux-generic/cls/vlan_run.sh -- 2.7.1.250.gff4ea60 diff --git a/test/linux-generic/.gitignore b/test/linux-generic/.gitignore index 5dabf91..f65c7c1 100644 --- a/test/linux-generic/.gitignore +++ b/test/linux-generic/.gitignore @@ -1,3 +1,4 @@ *.log *.trs tests-validation.env +test_out.pcap diff --git a/test/linux-generic/Makefile.am b/test/linux-generic/Makefile.am index f5cc52d..9acbab0 100644 --- a/test/linux-generic/Makefile.am +++ b/test/linux-generic/Makefile.am @@ -1,5 +1,8 @@ include $(top_srcdir)/test/Makefile.inc TESTS_ENVIRONMENT += TEST_DIR=${top_builddir}/test/common_plat/validation +TEST_EXTENSIONS = .sh + +dist_check_SCRIPTS = run-test tests-validation.env $(LOG_COMPILER) ALL_API_VALIDATION_DIR = ${top_builddir}/test/common_plat/validation/api @@ -37,11 +40,14 @@ TESTS = validation/api/pktio/pktio_run.sh \ SUBDIRS += validation/api/pktio\ validation/api/shmem\ + cls\ pktio_ipc\ ring if HAVE_PCAP TESTS += validation/api/pktio/pktio_run_pcap.sh +TESTS += cls/vlan_run.sh +dist_check_SCRIPTS += cls/vlan_run.sh cls/vlan.pcap endif if netmap_support TESTS += validation/api/pktio/pktio_run_netmap.sh @@ -61,10 +67,6 @@ SUBDIRS += validation/api/pktio endif endif -TEST_EXTENSIONS = .sh - -dist_check_SCRIPTS = run-test tests-validation.env $(LOG_COMPILER) - test_SCRIPTS = $(dist_check_SCRIPTS) tests-validation.env: diff --git a/test/linux-generic/cls/.gitignore b/test/linux-generic/cls/.gitignore new file mode 100644 index 0000000..9447625 --- /dev/null +++ b/test/linux-generic/cls/.gitignore @@ -0,0 +1,3 @@ +cls_main +*.log +*.trs diff --git a/test/linux-generic/cls/Makefile.am b/test/linux-generic/cls/Makefile.am new file mode 100644 index 0000000..43fb0bc --- /dev/null +++ b/test/linux-generic/cls/Makefile.am @@ -0,0 +1,13 @@ +include ../Makefile.inc + +noinst_LTLIBRARIES = libtestcls.la +libtestcls_la_SOURCES = cls.c +libtestcls_la_CFLAGS = $(AM_CFLAGS) $(INCCUNIT_COMMON) $(INCODP) + +test_PROGRAMS = cls_main$(EXEEXT) +dist_cls_main_SOURCES = cls_main.c + +cls_main_LDFLAGS = $(AM_LDFLAGS) +cls_main_LDADD = libtestcls.la $(LIBCUNIT_COMMON) $(LIBODP) + +noinst_HEADERS = cls_suites.h diff --git a/test/linux-generic/cls/cls.c b/test/linux-generic/cls/cls.c new file mode 100644 index 0000000..d527ec8 --- /dev/null +++ b/test/linux-generic/cls/cls.c @@ -0,0 +1,349 @@ +/* Copyright (c) 2016, Linaro Limited + * All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include "cls_suites.h" + +/** Get rid of path in filename - only for unix-type paths using '/' */ +#define NO_PATH(file_name) (strrchr((file_name), '/') ? \ + strrchr((file_name), '/') + 1 : (file_name)) + +/** + * Print usage information + */ +static void usage(char *progname) +{ + printf("\n" + "This is test application to verify that linux-generic classifier\n" + "correctly classifies packets on input. Main intend is add more code\n" + "coverage for internal functions playing different traffic recorded to\n" + "pcap files." + "\n" + "Usage: %s OPTIONS\n" + " E.g. %s -i pcap_file -e expected_bitmask\n" + "\n" + "Mandatory OPTIONS:\n" + " -i, pcap file name\n" + " -e, expected packet bitmask\n" + "\n", NO_PATH(progname), NO_PATH(progname) + ); +} + +/** @def SHM_PKT_POOL_SIZE + * @brief Size of the shared memory block + */ +#define SHM_PKT_POOL_SIZE 512 + +/** @def SHM_PKT_POOL_BUF_SIZE + * @brief Buffer size of the packet pool buffer + */ +#define SHM_PKT_POOL_BUF_SIZE 1856 + +/** + * Parsed command line application arguments + */ +typedef struct { + char *file_name; /**< File name for pcap pktio */ + odp_pktio_t pktio; /**< Pktio dev */ + input_flags_t expected_bits; /**< Expected bits from test run */ +} appl_args_t; + +/** + * Grouping of both parsed CL args and thread specific args - alloc together + */ +typedef struct { + /** Application (parsed) arguments */ + appl_args_t appl; +} args_t; + +/** Global pointer to args */ +static args_t *args; +/** Fill this bit struct to check which packet fields were classified */ +static input_flags_t packet_flags; + +/** + * Parse and store the command line arguments + * + * @param argc argument count + * @param argv[] argument vector + * @param appl_args Store application arguments here + */ +static void parse_args(int argc, char *argv[], appl_args_t *appl_args) +{ + int opt; + int long_index; + static const struct option longopts[] = { + {"interface", required_argument, NULL, 'i'}, /* return 'i' */ + {"extected", required_argument, NULL, 'e'}, /* return 'e' */ + {"help", no_argument, NULL, 'h'}, /* return 'h' */ + {NULL, 0, NULL, 0} + }; + + static const char *shortopts = ":i:e:h"; + + /* let helper collect its own arguments (e.g. --odph_proc) */ + odph_parse_options(argc, argv, shortopts, longopts); + + appl_args->file_name = NULL; + opterr = 0; /* do not issue errors on helper options */ + appl_args->expected_bits.all = 0; + + while (1) { + opt = getopt_long(argc, argv, shortopts, longopts, &long_index); + + if (opt == -1) + break; /* No more options */ + + switch (opt) { + case 'i': + appl_args->file_name = optarg; + break; + case 'e': + appl_args->expected_bits.all = atoll(optarg); + break; + + case 'h': + usage(argv[0]); + exit(EXIT_SUCCESS); + break; + default: + break; + } + } + + if (!appl_args->file_name) { + usage(argv[0]); + exit(EXIT_SUCCESS); + } + + optind = 1; /* reset 'extern optind' from the getopt lib */ +} + +/** + * Create a pktio handle, optionally associating a default input queue. + * + * @param dev Name of device to open + * @param pool Pool to associate with device for packet RX/TX + * @param mode Packet processing mode for this device (BURST or QUEUE) + * + * @return The handle of the created pktio object. + * @retval ODP_PKTIO_INVALID if the create fails. + */ +static odp_pktio_t create_pktio(const char *fname, odp_pool_t pool) +{ + odp_pktio_t pktio; + int ret; + odp_pktio_param_t pktio_param; + odp_pktin_queue_param_t pktin_param; + char dev[255]; + + odp_pktio_param_init(&pktio_param); + pktio_param.in_mode = ODP_PKTIN_MODE_SCHED; + + memset(dev, 0, 255); + sprintf(dev, "pcap:in=%s:out=test_out.pcap", fname); + + /* Open a packet IO instance */ + pktio = odp_pktio_open(dev, pool, &pktio_param); + if (pktio == ODP_PKTIO_INVALID) + LOG_ABORT("Error: pktio create failed for %s\n", dev); + + odp_pktin_queue_param_init(&pktin_param); + + pktin_param.queue_param.sched.sync = ODP_SCHED_SYNC_ATOMIC; + + if (odp_pktin_queue_config(pktio, &pktin_param)) + LOG_ABORT("Error: pktin config failed for %s\n", dev); + + if (odp_pktout_queue_config(pktio, NULL)) + LOG_ABORT("Error: pktout config failed for %s\n", dev); + + ret = odp_pktio_start(pktio); + if (ret != 0) + LOG_ABORT("Error: unable to start %s\n", dev); + + printf(" created pktio:%02" PRIu64 + ", dev:%s, queue mode (ATOMIC queues)\n" + " \tdefault pktio%02" PRIu64 "\n", + odp_pktio_to_u64(pktio), dev, + odp_pktio_to_u64(pktio)); + + return pktio; +} + +/** + * Packet IO loopback worker thread using ODP queues + * + * @param arg thread arguments of type 'thread_args_t *' + */ +static int pktio_queue_thread(void *arg ODP_UNUSED) +{ + int thr = odp_thread_id(); + odp_pktout_queue_t pktout; + odp_packet_t pkt; + odp_event_t ev; + uint64_t sched_wait = odp_schedule_wait_time(ODP_TIME_MSEC_IN_NS * 100); + + packet_flags.all = 0; + + /* Loop packets */ + while (1) { + odp_pktio_t pktio_tmp; + + ev = odp_schedule(NULL, sched_wait); + if (ev == ODP_EVENT_INVALID) + break; + + pkt = odp_packet_from_event(ev); + if (!odp_packet_is_valid(pkt)) + continue; + + pktio_tmp = odp_packet_input(pkt); + + if (odp_pktout_queue(pktio_tmp, &pktout, 1) != 1) { + LOG_ERR(" [%02i] Error: no pktout queue\n", thr); + return -1; + } + + /* Extend bits here for more additional tests */ + if (odp_packet_has_l2(pkt)) + packet_flags.parsed_l2 = 1; + if (odp_packet_has_vlan(pkt)) + packet_flags.vlan = 1; + + /* Enqueue the packet for output */ + if (odp_pktout_send(pktout, &pkt, 1) != 1) { + LOG_ERR(" [%i] Packet send failed.\n", thr); + odp_packet_free(pkt); + continue; + } + } + + return 0; +} + +int cls_main(int argc, char *argv[]) +{ + odph_odpthread_t thread_tbl[1]; + odp_pool_t pool; + int num_workers; + int i; + int cpu; + odp_cpumask_t cpumask; + char cpumaskstr[ODP_CPUMASK_STR_SIZE]; + odp_pool_param_t params; + odp_instance_t instance; + odph_odpthread_params_t thr_params; + + args = calloc(1, sizeof(args_t)); + if (args == NULL) { + LOG_ERR("Error: args mem alloc failed.\n"); + exit(EXIT_FAILURE); + } + + parse_args(argc, argv, &args->appl); + + /* Init ODP before calling anything else */ + if (odp_init_global(&instance, NULL, NULL)) { + LOG_ERR("Error: ODP global init failed.\n"); + exit(EXIT_FAILURE); + } + + /* Init this thread */ + if (odp_init_local(instance, ODP_THREAD_CONTROL)) { + LOG_ERR("Error: ODP local init failed.\n"); + exit(EXIT_FAILURE); + } + + num_workers = odp_cpumask_default_worker(&cpumask, 1); + (void)odp_cpumask_to_str(&cpumask, cpumaskstr, sizeof(cpumaskstr)); + + /* Create packet pool */ + odp_pool_param_init(¶ms); + params.pkt.seg_len = SHM_PKT_POOL_BUF_SIZE; + params.pkt.len = SHM_PKT_POOL_BUF_SIZE; + params.pkt.num = SHM_PKT_POOL_SIZE; + params.type = ODP_POOL_PACKET; + + pool = odp_pool_create("packet_pool", ¶ms); + if (pool == ODP_POOL_INVALID) { + LOG_ERR("Error: packet pool create failed.\n"); + exit(EXIT_FAILURE); + } + + args->appl.pktio = create_pktio(args->appl.file_name, pool); + + /* Create and init worker threads */ + memset(thread_tbl, 0, sizeof(thread_tbl)); + + memset(&thr_params, 0, sizeof(thr_params)); + thr_params.thr_type = ODP_THREAD_WORKER; + thr_params.instance = instance; + + cpu = odp_cpumask_first(&cpumask); + for (i = 0; i < num_workers; ++i) { + odp_cpumask_t thd_mask; + int (*thr_run_func)(void *); + + thr_run_func = pktio_queue_thread; + /* + * Create threads one-by-one instead of all-at-once, + * because each thread might get different arguments. + * Calls odp_thread_create(cpu) for each thread + */ + odp_cpumask_zero(&thd_mask); + odp_cpumask_set(&thd_mask, cpu); + + thr_params.start = thr_run_func; + thr_params.arg = &args; + + odph_odpthreads_create(&thread_tbl[i], &thd_mask, &thr_params); + cpu = odp_cpumask_next(&cpumask, cpu); + } + + /* Master thread waits for other threads to exit */ + for (i = 0; i < num_workers; ++i) + odph_odpthreads_join(&thread_tbl[i]); + + odp_pktio_stop(args->appl.pktio); + odp_pktio_close(args->appl.pktio); + + while (1) { + odp_event_t ev; + + ev = odp_schedule(NULL, 0); + if (ev == ODP_EVENT_INVALID) + break; + } + + odp_pool_destroy(pool); + odp_term_local(); + odp_term_global(instance); + + if (packet_flags.all != args->appl.expected_bits.all) { + LOG_ERR("Flags %" PRIu64 " expected %" PRIu64 "\n", + packet_flags.all, args->appl.expected_bits.all); + free(args); + return -1; + } + + free(args); + return 0; +} diff --git a/test/linux-generic/cls/cls_main.c b/test/linux-generic/cls/cls_main.c new file mode 100644 index 0000000..40179b9 --- /dev/null +++ b/test/linux-generic/cls/cls_main.c @@ -0,0 +1,12 @@ +/* Copyright (c) 2016, Linaro Limited + * All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "cls_suites.h" + +int main(int argc, char *argv[]) +{ + return cls_main(argc, argv); +} diff --git a/test/linux-generic/cls/cls_suites.h b/test/linux-generic/cls/cls_suites.h new file mode 100644 index 0000000..94c9b55 --- /dev/null +++ b/test/linux-generic/cls/cls_suites.h @@ -0,0 +1 @@ +int cls_main(int argc, char *argv[]); diff --git a/test/linux-generic/cls/vlan.pcap b/test/linux-generic/cls/vlan.pcap new file mode 100644 index 0000000000000000000000000000000000000000..106ccb682e51495b4025337518a0bce63c2c7681 GIT binary patch literal 9728 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