Received: by 2002:a25:e7d8:0:0:0:0:0 with SMTP id e207csp2978233ybh; Mon, 16 Mar 2020 13:23:05 -0700 (PDT) X-Google-Smtp-Source: ADFU+vuxUEzwJY8fpFKpxsrga+YYThYf1oz6ZFQ/8y3zW+KM/CTuVQlcL17p2akh/L8AOp3iiPwp X-Received: by 2002:aca:5354:: with SMTP id h81mr952816oib.164.1584390185349; Mon, 16 Mar 2020 13:23:05 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1584390185; cv=none; d=google.com; s=arc-20160816; b=HqU5fmJR0kbbMpUp1zg/vVeBKw0L6XyN4eUukYpB4fu7rC+dZf7uoSVYDggXXHQ4zC Jbw47sSZnWFB4kh/Xz2mIzvrYTA6FOy3lJxRt6S5sd5xRQ1LDSqpcE7gpUM2sU2787gl fdbMRqpZdK7nfh0tE8opmDQoH39CYEuUoKsSoZadfLW0c8vDrK04OlxhAQBDcupV4ix5 PTk0UN4vEJ94lpvpI4fw5tfjYkUg6MxHL6CFx4f8j4cFEZuih6INLaAQVktAsAbqrR21 yaXmP4qh7c+HWNXhCoAn1eM5Yb+C93Y9Ws42ewBsTl7iSUGcbXYbCNGxwi9/hNronbyU 4eZQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=list-id:precedence:sender:cc:to:from:subject:mime-version :message-id:date:dkim-signature; bh=0dKXpM+vAToFQ99KFoY4s/h1UvCiGXVGJzVOlRPgN/w=; b=laYpTDdDzuTEgfYwJJBb6ul78A9bm5QLTA709K5StsTIrVQWiRIosgOJAHXeyUBRVL plba28OUZPhJ5zWdhKdYHdsB2GNaefwqaNFuPP9aBeAN4GVJrFz6X4t36RBwyo1zlh/r rJzItLZgSz0tFOPnY3eb8GHgFQxbxtnGOMoVxgm1Ac1VJT6mAZriupxtZ/pjIDD9fChU pEIoFsULTyEpwtSW6N3b6HwZPH5tOr43QWOJMdxBZZ6Fvm91gbnl2QnN74d3oB6xCVNQ L0kd16SFuQmdvqOl0fnbS2BJIr6FspWxXg/4ltMVNzRdn8GG3lM/TgjX6X4JleG1j8aJ /y1w== ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@google.com header.s=20161025 header.b=lL1UaDJ3; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=google.com Return-Path: Received: from vger.kernel.org (vger.kernel.org. [209.132.180.67]) by mx.google.com with ESMTP id w7si474078otj.281.2020.03.16.13.22.53; Mon, 16 Mar 2020 13:23:05 -0700 (PDT) Received-SPF: pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) client-ip=209.132.180.67; Authentication-Results: mx.google.com; dkim=pass header.i=@google.com header.s=20161025 header.b=lL1UaDJ3; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=google.com Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1732518AbgCPUVq (ORCPT + 99 others); Mon, 16 Mar 2020 16:21:46 -0400 Received: from mail-pf1-f201.google.com ([209.85.210.201]:43554 "EHLO mail-pf1-f201.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1732486AbgCPUVp (ORCPT ); Mon, 16 Mar 2020 16:21:45 -0400 Received: by mail-pf1-f201.google.com with SMTP id 20so12737756pfw.10 for ; Mon, 16 Mar 2020 13:21:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:message-id:mime-version:subject:from:to:cc; bh=0dKXpM+vAToFQ99KFoY4s/h1UvCiGXVGJzVOlRPgN/w=; b=lL1UaDJ33tqQdzKUxp7HqRoiLyuZO479lvU6K9jUmKLpg0o1p4LSgewSvJ2wJzWsYK RVeji6SAfZdkg8w/OOlb/aA5GppJiCyzOIuTDimNye627x0kRPDMO/sVk0Ka/uQMLYGI V8sbQrXBatkbxDemqrL+u4d1aTYPkVNi8PM7Ahm8XZDhy4T2IXt04dCDAOmnJclbME92 z2eXHlgn6AfKzhPa5Pujnaja7PShVvF7S0L6WAPUX6EpwT9ZudwrRuaAoneqKLhbXouO bXHeb6ZYoEZZbQUJn2C7DSsftxZAP7oUeRO9KtsztW9zQ+fmMas08bfd9uzqAZqq1g10 N8XA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:message-id:mime-version:subject:from:to:cc; bh=0dKXpM+vAToFQ99KFoY4s/h1UvCiGXVGJzVOlRPgN/w=; b=gd8TJ3cqCY6nBt3K39IR9DhbLXNP1SIksMynN87ZvcWlse1yqgtGKwtGUAaaP4OjUd aHjW4GhL/LHlku7NYgUjbjVsZYgtcDxJvdk7R8l6555h4E2d8rQzyebd2q1v+si5XzEj RKooL10fvbD1LrOrkcw5VKOOQUgcJoYeOksbvzKnMvtOEzv5jVX9/wAb3jlbLsjQpGaE F0W24qXyA/l7GrzeeNkXo2RJ8HuczxXcTnAOdxUzKyMOWPyJFEyHN6tukjKfzC0dEedN xieFYTrbMBhpgwgjwzWcLJhy9E9hgW4oj6xXbE5mTVfGcqBGo0k4mmszR6t5xnIwWn2N +7UA== X-Gm-Message-State: ANhLgQ2P8Gi1EeJvj+YpEdrBzyd/5nUXincWqvBtjQAb+gI2eTM9VdGn If9YUaAYIpOPs3QVmFn6lhcxT2n7/w1GoNbl X-Received: by 2002:a17:90b:3683:: with SMTP id mj3mr1254907pjb.153.1584390103472; Mon, 16 Mar 2020 13:21:43 -0700 (PDT) Date: Mon, 16 Mar 2020 13:21:24 -0700 Message-Id: <20200316202125.15852-1-heidifahim@google.com> Mime-Version: 1.0 X-Mailer: git-send-email 2.25.1.481.gfbce0eb801-goog Subject: [PATCH v3 1/2] kunit: kunit_parser: make parser more robust From: Heidi Fahim To: shuah@kernel.org, brendanhiggins@google.com, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, kunit-dev@googlegroups.com Cc: Heidi Fahim Content-Type: text/plain; charset="UTF-8" Sender: linux-kernel-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Previously, kunit_parser did not properly handle kunit TAP output that - had any prefixes (generated from different configs e.g. CONFIG_PRINTK_TIME) - had unrelated kernel output mixed in the middle of it, which has shown up when testing with allyesconfig To remove prefixes, the parser looks for the first line that includes TAP output, "TAP version 14". It then determines the length of the string before this sequence, and strips that number of characters off the beginning of the following lines until the last KUnit output line is reached. These fixes have been tested with additional tests in the KUnitParseTest and their associated logs have also been added. Signed-off-by: Heidi Fahim --- Changelog v3: - Addressing Shuah's git apply issues with whitespaces. Marked all files in test_data as binaries in new kunit/ .gitattributes file. The irregular whitespaces here are necessary as they are used for testing the KUnit parser. - Important: please note that now git apply works, however the .log files in /test_data/ may get marked as Untracked files. These should be included in the commit as they are required for kunit_tool_test.py. tools/testing/kunit/.gitattributes | 1 + tools/testing/kunit/kunit_parser.py | 40 +++++----- tools/testing/kunit/kunit_tool_test.py | 69 ++++++++++++++++++ .../test_data/test_config_printk_time.log | Bin 0 -> 1584 bytes .../test_data/test_interrupted_tap_output.log | Bin 0 -> 1982 bytes .../test_data/test_kernel_panic_interrupt.log | Bin 0 -> 1321 bytes .../test_data/test_multiple_prefixes.log | Bin 0 -> 1832 bytes ..._output_with_prefix_isolated_correctly.log | Bin 0 -> 1655 bytes .../kunit/test_data/test_pound_no_prefix.log | Bin 0 -> 1193 bytes .../kunit/test_data/test_pound_sign.log | Bin 0 -> 1655 bytes 10 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 tools/testing/kunit/.gitattributes create mode 100644 tools/testing/kunit/test_data/test_config_printk_time.log create mode 100644 tools/testing/kunit/test_data/test_interrupted_tap_output.log create mode 100644 tools/testing/kunit/test_data/test_kernel_panic_interrupt.log create mode 100644 tools/testing/kunit/test_data/test_multiple_prefixes.log create mode 100644 tools/testing/kunit/test_data/test_output_with_prefix_isolated_correctly.log create mode 100644 tools/testing/kunit/test_data/test_pound_no_prefix.log create mode 100644 tools/testing/kunit/test_data/test_pound_sign.log diff --git a/tools/testing/kunit/.gitattributes b/tools/testing/kunit/.gitattributes new file mode 100644 index 000000000000..5b7da1fc3b8f --- /dev/null +++ b/tools/testing/kunit/.gitattributes @@ -0,0 +1 @@ +test_data/* binary diff --git a/tools/testing/kunit/kunit_parser.py b/tools/testing/kunit/kunit_parser.py index 4ffbae0f6732..adf86747b07f 100644 --- a/tools/testing/kunit/kunit_parser.py +++ b/tools/testing/kunit/kunit_parser.py @@ -46,19 +46,21 @@ class TestStatus(Enum): TEST_CRASHED = auto() NO_TESTS = auto() -kunit_start_re = re.compile(r'^TAP version [0-9]+$') -kunit_end_re = re.compile('List of all partitions:') +kunit_start_re = re.compile(r'TAP version [0-9]+$') +kunit_end_re = re.compile('(List of all partitions:|' + 'Kernel panic - not syncing: VFS:|reboot: System halted)') def isolate_kunit_output(kernel_output): started = False for line in kernel_output: - if kunit_start_re.match(line): + if kunit_start_re.search(line): + prefix_len = len(line.split('TAP version')[0]) started = True - yield line - elif kunit_end_re.match(line): + yield line[prefix_len:] if prefix_len > 0 else line + elif kunit_end_re.search(line): break elif started: - yield line + yield line[prefix_len:] if prefix_len > 0 else line def raw_output(kernel_output): for line in kernel_output: @@ -108,18 +110,16 @@ OK_NOT_OK_SUBTEST = re.compile(r'^\t(ok|not ok) [0-9]+ - (.*)$') OK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) [0-9]+ - (.*)$') -def parse_ok_not_ok_test_case(lines: List[str], - test_case: TestCase, - expecting_test_case: bool) -> bool: +def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool: save_non_diagnositic(lines, test_case) if not lines: - if expecting_test_case: - test_case.status = TestStatus.TEST_CRASHED - return True - else: - return False + test_case.status = TestStatus.TEST_CRASHED + return True line = lines[0] match = OK_NOT_OK_SUBTEST.match(line) + while not match and lines: + line = lines.pop(0) + match = OK_NOT_OK_SUBTEST.match(line) if match: test_case.log.append(lines.pop(0)) test_case.name = match.group(2) @@ -150,12 +150,12 @@ def parse_diagnostic(lines: List[str], test_case: TestCase) -> bool: else: return False -def parse_test_case(lines: List[str], expecting_test_case: bool) -> TestCase: +def parse_test_case(lines: List[str]) -> TestCase: test_case = TestCase() save_non_diagnositic(lines, test_case) while parse_diagnostic(lines, test_case): pass - if parse_ok_not_ok_test_case(lines, test_case, expecting_test_case): + if parse_ok_not_ok_test_case(lines, test_case): return test_case else: return None @@ -234,11 +234,11 @@ def parse_test_suite(lines: List[str]) -> TestSuite: expected_test_case_num = parse_subtest_plan(lines) if not expected_test_case_num: return None - test_case = parse_test_case(lines, expected_test_case_num > 0) - expected_test_case_num -= 1 - while test_case: + while expected_test_case_num > 0: + test_case = parse_test_case(lines) + if not test_case: + break test_suite.cases.append(test_case) - test_case = parse_test_case(lines, expected_test_case_num > 0) expected_test_case_num -= 1 if parse_ok_not_ok_test_suite(lines, test_suite): test_suite.status = bubble_up_test_case_errors(test_suite) diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py index cba97756ac4a..0efae697f396 100755 --- a/tools/testing/kunit/kunit_tool_test.py +++ b/tools/testing/kunit/kunit_tool_test.py @@ -108,6 +108,36 @@ class KUnitParserTest(unittest.TestCase): self.assertContains('ok 1 - example', result) file.close() + def test_output_with_prefix_isolated_correctly(self): + log_path = get_absolute_path( + 'test_data/test_pound_sign.log') + with open(log_path) as file: + result = kunit_parser.isolate_kunit_output(file.readlines()) + self.assertContains('TAP version 14\n', result) + self.assertContains(' # Subtest: kunit-resource-test', result) + self.assertContains(' 1..5', result) + self.assertContains(' ok 1 - kunit_resource_test_init_resources', result) + self.assertContains(' ok 2 - kunit_resource_test_alloc_resource', result) + self.assertContains(' ok 3 - kunit_resource_test_destroy_resource', result) + self.assertContains(' foo bar #', result) + self.assertContains(' ok 4 - kunit_resource_test_cleanup_resources', result) + self.assertContains(' ok 5 - kunit_resource_test_proper_free_ordering', result) + self.assertContains('ok 1 - kunit-resource-test', result) + self.assertContains(' foo bar # non-kunit output', result) + self.assertContains(' # Subtest: kunit-try-catch-test', result) + self.assertContains(' 1..2', result) + self.assertContains(' ok 1 - kunit_test_try_catch_successful_try_no_catch', + result) + self.assertContains(' ok 2 - kunit_test_try_catch_unsuccessful_try_does_catch', + result) + self.assertContains('ok 2 - kunit-try-catch-test', result) + self.assertContains(' # Subtest: string-stream-test', result) + self.assertContains(' 1..3', result) + self.assertContains(' ok 1 - string_stream_test_empty_on_creation', result) + self.assertContains(' ok 2 - string_stream_test_not_empty_after_add', result) + self.assertContains(' ok 3 - string_stream_test_get_string', result) + self.assertContains('ok 3 - string-stream-test', result) + def test_parse_successful_test_log(self): all_passed_log = get_absolute_path( 'test_data/test_is_test_passed-all_passed.log') @@ -150,6 +180,45 @@ class KUnitParserTest(unittest.TestCase): result.status) file.close() + def test_ignores_prefix_printk_time(self): + prefix_log = get_absolute_path( + 'test_data/test_config_printk_time.log') + with open(prefix_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + + def test_ignores_multiple_prefixes(self): + prefix_log = get_absolute_path( + 'test_data/test_multiple_prefixes.log') + with open(prefix_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + + def test_prefix_mixed_kernel_output(self): + mixed_prefix_log = get_absolute_path( + 'test_data/test_interrupted_tap_output.log') + with open(mixed_prefix_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + + def test_prefix_poundsign(self): + pound_log = get_absolute_path('test_data/test_pound_sign.log') + with open(pound_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + + def test_kernel_panic_end(self): + panic_log = get_absolute_path('test_data/test_kernel_panic_interrupt.log') + with open(panic_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + + def test_pound_no_prefix(self): + pound_log = get_absolute_path('test_data/test_pound_no_prefix.log') + with open(pound_log) as file: + result = kunit_parser.parse_run_tests(file.readlines()) + self.assertEqual('kunit-resource-test', result.suites[0].name) + class StrContains(str): def __eq__(self, other): return self in other diff --git a/tools/testing/kunit/test_data/test_config_printk_time.log b/tools/testing/kunit/test_data/test_config_printk_time.log new file mode 100644 index 0000000000000000000000000000000000000000..c02ca773946d641291e27d44d73174cc16a17d9d GIT binary patch literal 1584 zcmai!-)q}25XYapzv7@T8>=%piIdE*htV+@8zqo+FQEuUKBp$O&PeiF|Mxq^U79*h zs}~FTKHvK#-5Pj_k(dcycTn5H_+1K`jH41^UFj&k12k$=V)kzR(%LGMQI*0Lz3ldK z(|UBERmtX%TdJTig_lINlSTGXJWi5N;&CeAq44*ht=Y<8@I~~se}7%VU$m|u1M#%~ z_u>*(&4yk16m|^@L>3)2R$~+nPt4=dC^y^)CYDjs4Rz;38qZzYxAj@0F+G)x&<*QwCyL`dCmh7*>d7vEleL$_OFG2z??d#nTljtjZ9ZkNm*D-^a#H~OYwd7vmgtfWvPH*5DjZL(I!MOc* zZy)C6f=sy((vk=QgI1SDV&H{a>VtlNvL)0h3?OLm{2!iseC^X(WV?{V9Y~5?%QdtM zIog4Y$gt7XPHcL5F%RLE^7>OapbwB)g37@&>kq)-GO-S+e#?H_>cu=b6LN(iT%(IE z9EjK+tP#FY+EC5piok8`;o%`XoL*g?pWgid?*fpdt@Pa)qRq63FzQiNXhFU;Fujx0hFd>uD?GdFr`uyIxOliVuJ{_5EAR0Ab2aCJuu!;NtvNgWwEYgCwQ_0 zTo_%T;ob|bO;lI39l>Vje$e?_gW?S3S~$7dIVr=oxxGz$&43UKuUWV)r9^AvGUHyT zbR+NLc$=Ae$lf?g)nbV>+HM|lWla-&5X(mFG+I>QfLcVn zXo%pNY&qd~OaznuxX%J32egsIFp7>On;lIsh!W!}*xzdUxf3#N#|AKELpGbnGdc_w rayVsh#3s_;p6f%~!%YmE#1iFNm430IoQg#U=CA6uosqXj&w%{_Pb*9~ literal 0 HcmV?d00001 diff --git a/tools/testing/kunit/test_data/test_kernel_panic_interrupt.log b/tools/testing/kunit/test_data/test_kernel_panic_interrupt.log new file mode 100644 index 0000000000000000000000000000000000000000..c045eee75f27fefaabf3ba073d9e282721c19a67 GIT binary patch literal 1321 zcmai!-)q}25XYapzv7@T8=EuPiIdc@htV+@gAvHOmr#T(%c-%gGm^a2|NYK!=cfMA z>c!%GpYPr2bUFYnwqR>U>mDkTX_rSJQCln&97^f=_n@?h3zfV(y!19gCs;;sRE~Fh z9yd-|kfI=WNDY>d>CAD&%JMAv$R3yBXZE;aaLU{RNTZLub@;-D+P}Z<;IA?^Lk<3F z`1k4>_H|65Tm*Sd0-jM8Ya^A{8b|K_ViAsvn1c^q$MgR@Z~1dhy~$3I!3;^2qwLWs zaydgL6xd+%D!ZCtWub(wtDc=qIh=tjgOxHoHA-wazu3v=`I;&EMZ_Kd}!({oCDL1PpdRzC|=Sw^-8b z+mop1_P{R^a)MqDn)`6ES}@N@-#aQ1B*{jy?Iu)Sk{Q>)^{Cr>CuANCqrsX5Y`adk qwDDsZtQl-rDBFjs-@L}KFqKO7uB5^OxfbyR^W-{?vuvZ$^4UKw_JoN5 literal 0 HcmV?d00001 diff --git a/tools/testing/kunit/test_data/test_multiple_prefixes.log b/tools/testing/kunit/test_data/test_multiple_prefixes.log new file mode 100644 index 0000000000000000000000000000000000000000..bc48407dcc36c44665c7d2ac620e42e7caf98481 GIT binary patch literal 1832 zcma)-UvJYe5Wt_ur#NXZAeQ2^O;;lEfQFC|XsWc0mua$G=h8(@e39+2<=b;^pet<7 z>X$ed-|znUF1Nr-OvFO)y1NZ;*XbR!wlaR3gC^ssM!0Qs!eTUPU1RZPcka2CRi>aO zhYfl;?B%-m=s>HQ)u6Q0KxHZ~iCQPC;=OpBWgo=jT);!+??GF$k>S865wmgqc?G}F zx{3^>^Vq4AQ@HBZY})6r?HHI?bktdmNf1WW(#a%?exxm=kPJNemnQ>n=ks3V#t730 zfipyUj~FA&B7}k+vD6>Mm`518MrFEo&ln9Qi$GdSEm~PxL`k+lzDt`}K!-U!vPREJ z>usNC<@NomwVlS?I63rVC@E9rjw)TJH9A*zwG2W-gM5tjROQ|Ecw%+tKUzWP#(C`8 z9+w;$>p_nAQgHqgexPbz&CqNts&M-UoE&gNrm$&!FUd&FCOkLh7$b!80>hA(14osf zFOMpsQzq0Z^uP<0Fn9)W%!Pk+k4egp%6Xs?xIdv))>k1wm&f>*1Z7pD>z&7j$7fd; z&`@VQSR#K0TnsE?+A-*Nv<8EDQ)v#Ia|Nz9n#-EQ&#zZG-0bEucsj5Jr30Rko!J_C zFv*(7%l7TZ<(YUFGjp-J$w9#C+n1aN-32u~%H4x%xtZaTZFzsWIhpz)oHRp zH7$yBEtcmQCPig^52w*4yYfnJW3dA)#8fO7<&rn)T2B`O&PArfe`&vod48xXvQneo XtMDf=)eE&A(fy zUt&$>z4`Rp106PFD@Nx5DwAt>j6tHdSSZ++G6)|)X^|8veY1b*ZG=v-jNzahZ|^*5 zoU$NALGF+lEFssqT z!#;1vGe@mEEGa5G%tNVMXoCOLa#cI+1ob?ij}ql*<%^-L{c^W(-oo1ssIqbmN6jTs zI(lD2SfA=X@ICh{K7jT*(VDv$Nxmzil@FtHNd-~uU>hxI95MX*x{Kld zEQ{dqiW@QuBw4jSYCP#6AvHL@{kXYd@4EE2+xr+8Y`=YpNr!H+Bqg;cQPG2gJBsB5 zX)&p2G+WMCU}P8^l$fXKTC&YLQbC#<*TA&5^gAcyv0EF!ip|+(m2Kz}CUU-Fux63; jvVXaBGrY#ID3eNFTtt2D7x%05gScM{xU2jvXlo8~06vKbetq7-KeVnQ1IapqHy5z)axUjHICc!h z79Dj~V?1m!kSrD{zvu`F#DS5Ij2xJ;_O#^!yFA0PXVMcK*wq=f;Nq7084V>7Kc{l+DZE0Dm{a@mRhv3w1|>yf&57h5zMdrJeHXgFl6E}JD|?D-4NP~RK4wE zt-QXS#kvf4IK?`q$)p@f={l{^xw5O}uxTjl2h}OyXR6LT(-nm7QOQ@Dgz*bcGuNCv zXAtuXRWtLvI(ZI=3`pn%Y+Bz-GE%e5bB~?8zCs)FGALB(Iet}eyAO>vG{h_XFacqE z{5B`*sm&ji^FSqVt3j)*uLdJ7i{K|gS=H!z=ds~=>*@j;>Wl|V`)nb>?X3&cr$Q`SIDZMAulOIpvz#v03lUqL#0ON%qPWsxdzE5bRMz)!!40$bN+0Q%a zUt&$>z4`Rp106PFD@Nx5DwAt>j6tHdSSZ++G6)|)X^|8veY1b*ZG=v-jNzahZ|^*5 zoU$NALGF+lEFssqT z!#;1vGe@mEEGa5G%tNVMXoCOLa#cI+1ob?ij}ql*<%^-L{c^W(-oo1ssIqbmN6jTs zI(lD2SfA=X@ICh{K7jT*(VDv$Nxmzil@FtHNd-~uU>hxI95MX*x{Kld zEQ{dqiW@QuBw4jSYCP#6AvHL@{kXYd@4EE2+xr+8Y`=YpNr!H+Bqg;cQPG2gJBsB5 zX)&p2G+WMCU}P8^l$fXKTC&YLQbC#<*TA&5^gAcyv0EF!ip|+(m2Kz}CUU-Fux63; jvVXaBGrY#ID3eNFT