From ea149e36f79c10d1667866fb30537c0c7934a50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 11:43:49 +0200 Subject: [PATCH 01/20] Add bare testing on Linux --- .github/workflows/main.yml | 54 +++++++--- .gitignore | 6 +- README.md | 18 ++-- requirements.txt | 1 + test/TestException.c | 74 +++++++++++++ test/support/CExceptionConfig.h | 7 ++ waf | 177 ++++++++++++++++++++++++++++++++ wscript | 142 +++++++++++++++++++++++++ 8 files changed, 457 insertions(+), 22 deletions(-) create mode 100644 requirements.txt create mode 100644 waf create mode 100644 wscript diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1646a11..d3f0db9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,28 +6,58 @@ name: CI # Triggers the workflow on push or pull request events but only for the master branch on: push: - branches: [ master ] pull_request: branches: [ master ] jobs: # Job: Unit test suite unit-tests: - name: "Unit Tests" + # name: "Unit Tests" + # runs-on: ubuntu-latest + # steps: + # # Checks out repository under $GITHUB_WORKSPACE + # - name: Checkout Latest Repo + # uses: actions/checkout@v4 + # with: + # submodules: recursive + + # # Install Ceedling + # - name: Install Ceedling + # run: | + # sudo gem install ceedling + + # # Run Tests + # - name: Run All Self Tests + # run: | + # ceedling clobber test:all + + name: "Unit Tests (Bare GCC)" runs-on: ubuntu-latest steps: - # Checks out repository under $GITHUB_WORKSPACE - name: Checkout Latest Repo - uses: actions/checkout@v4 - with: - submodules: recursive + uses: actions/checkout@v7.0.1 - # Install Ceedling - - name: Install Ceedling + - name: Install Python 3 + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Create virtual environment run: | - sudo gem install ceedling + python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt - # Run Tests - - name: Run All Self Tests + - name: Run build and test run: | - ceedling clobber test:all + source .venv/bin/activate + python waf configure build -v \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1cfa3c8..969ed56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ build -*.sublime-* \ No newline at end of file +*.sublime-* +.ruff_cache/ +waf*-*-*/ +.lock-waf_* +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 3b05f24..0a119fe 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Mostly error handling. Passing errors down a long chain of function calls gets u So what if you could just specify certain places where you want to handle errors, and all your errors were transferred there? Let's try a lame example: -CException uses C standard library functions setjmp and longjmp to operate. As long as the target system +CException uses C standard library functions `setjmp` and `longjmp` to operate. As long as the target system has these two functions defined, this library should be useable with very little configuration. It even supports environments where multiple program flows are in use, such as real-time operating systems... we started this project for use in embedded systems... but it obviously can be used for larger systems too. @@ -42,7 +42,7 @@ void functionC(void) { } ``` -There are about a gajillion exception frameworks using a similar setjmp/longjmp method out there... and there +There are about a gajillion exception frameworks using a similar `setjmp`/`longjmp` method out there... and there will probably be more in the future. Unfortunately, when we started our last embedded project, all those that existed either (a) did not support multiple tasks (therefore multiple stacks) or (b) were way more complex than we really wanted. CException was born. @@ -63,7 +63,7 @@ in passing objects or structs or strings... just simple error codes. Fast. Easy ### Performance... CException can be configured for single tasking or multitasking. In single tasking, there is -very little overhead past the setjmp/longjmp calls (which are already fast). In multitasking, your only additional +very little overhead past the `setjmp`/`longjmp` calls (which are already fast). In multitasking, your only additional overhead is the time it takes you to determine a unique task id (0 to num_tasks). How? @@ -82,7 +82,7 @@ function calls (nested as deeply as you like). There can be as many Throws as yo execution of the guts of your Try block ends as soon as the first Throw is triggered. Once you throw, you're transferred to the Catch block. A silly example: -``` +```c void SillyExampleWhichPrintsZeroThroughFive(void) { volatile CEXCEPTION_T e; int i; @@ -109,7 +109,7 @@ successfully utilize this library: Do not directly `return` from within a `Try` block, nor `goto` into or out of a `Try` block. The `Try` macro allocates some local memory and alters a global pointer. These are cleaned up at the -top of the `Catch` macro. Gotos and returns would bypass some of these steps, resulting in memory leaks +top of the `Catch` macro. `goto`s and returns would bypass some of these steps, resulting in memory leaks or unpredictable behavior. ### Local Variables @@ -174,7 +174,7 @@ Configuration CException is a mostly portable library. It has one universal dependency, plus some macros which are required if working in a multi-tasking environment. -The standard C library setjmp must be available. Since this is part of the standard library, it's all good. +The standard C library `setjmp` must be available. Since this is part of the standard library, it's all good. If working in a multitasking environment, you need a stack frame for each task. Therefore, you must define methods for obtaining an index into an array of frames and to get the overall number of id's are required. If @@ -229,8 +229,8 @@ particular places. It is a rare instance where you will need these, but they are Testing ======= -If you want to validate that CException works with your tools or that it works with your custom +If you want to validate that `CException` works with your tools or that it works with your custom configuration, you may want to run the included test suite. This is the test suite (along with real projects we've used it on) that we use to make sure that things actually work the way we claim. -The test suite makes use of Ceedling, which uses the Unity Test Framework. It will require a native C compiler. -The example makefile and rakefile both use gcc. +The test suite makes use of `Ceedling`, which uses the Unity Test Framework. It will require a native C compiler. +The example makefile and `rakefile` both use gcc. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3383960 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +gcovr==8.6 diff --git a/test/TestException.c b/test/TestException.c index d339a78..a3386c5 100644 --- a/test/TestException.c +++ b/test/TestException.c @@ -5,9 +5,34 @@ SPDX-License-Identifier: MIT ========================================================================= */ +#ifndef BARE_TEST #include "unity.h" +#endif // BARE_TEST #include "CException.h" +#ifdef BARE_TEST +#include +#include +#include +#endif // BARE_TEST + +#ifdef BARE_TEST + +#ifndef TEST_FAIL_MESSAGE +#define TEST_FAIL_MESSAGE(x) fprintf(stderr, "%s", x) +#endif // TEST_FAIL_MESSAGE + +void TEST_ASSERT_EQUAL(unsigned int a, CEXCEPTION_T b) { + assert(a == b); +} + +void TEST_ASSERT_FALSE(unsigned int a) { + if (a != false) { + assert(false); + } +} +#endif // BARE_TEST + volatile int TestingTheFallback; volatile int TestingTheFallbackId; @@ -313,6 +338,9 @@ void test_CanHaveNestedTryBlocksInASingleFunction_ThrowOutside(void) TEST_ASSERT_EQUAL(2, i); } +#ifdef CEXCEPTION_USE_CONFIG_FILE +// This test only makes sense to run when CException was built using the +// customized configuration test/support/CExceptionConfig.h void test_AThrowWithoutATryCatchWillUseDefaultHandlerIfSpecified(void) { //Let the fallback handler know we're expecting it to get called this time, so don't fail @@ -324,7 +352,11 @@ void test_AThrowWithoutATryCatchWillUseDefaultHandlerIfSpecified(void) TEST_ASSERT_FALSE(TestingTheFallback); TEST_ASSERT_EQUAL(0xBE, TestingTheFallbackId); } +#endif // CEXCEPTION_USE_CONFIG_FILE +#ifdef CEXCEPTION_USE_CONFIG_FILE +// This test only makes sense to run when CException was built using the +// customized configuration test/support/CExceptionConfig.h void test_AThrowWithoutOutsideATryCatchWillUseDefaultHandlerEvenAfterTryCatch(void) { CEXCEPTION_T e; @@ -347,6 +379,7 @@ void test_AThrowWithoutOutsideATryCatchWillUseDefaultHandlerEvenAfterTryCatch(vo TEST_ASSERT_FALSE(TestingTheFallback); TEST_ASSERT_EQUAL(0xBE, TestingTheFallbackId); } +#endif // CEXCEPTION_USE_CONFIG_FILE void test_AbilityToExitTryWithoutThrowingAnError(void) { @@ -396,3 +429,44 @@ void test_AbilityToExitTryWillOnlyExitOneLevel(void) // verify that we picked up and ran after first Try TEST_ASSERT_EQUAL(1, i); } + +#ifdef BARE_TEST +int main() { + printf("SetUp\n"); + setUp(); + printf("test_BasicTryDoesNothingIfNoThrow\n"); + test_BasicTryDoesNothingIfNoThrow(); + printf("test_BasicThrowAndCatch\n"); + test_BasicThrowAndCatch(); + printf("test_BasicThrowAndCatch_WithMiniSyntax\n"); + test_BasicThrowAndCatch_WithMiniSyntax(); + printf("test_VerifyVolatilesSurviveThrowAndCatch\n"); + test_VerifyVolatilesSurviveThrowAndCatch(); + printf("test_ThrowFromASubFunctionAndCatchInRootFunc\n"); + test_ThrowFromASubFunctionAndCatchInRootFunc(); + printf("test_ThrowAndCatchFromASubFunctionAndRethrowToCatchInRootFunc\n"); + test_ThrowAndCatchFromASubFunctionAndRethrowToCatchInRootFunc(); + printf("test_ThrowAndCatchFromASubFunctionAndNoRethrowToCatchInRootFunc\n"); + test_ThrowAndCatchFromASubFunctionAndNoRethrowToCatchInRootFunc(); + printf("test_ThrowAnErrorThenEnterATryBlockFromWithinCatch_VerifyThisDoesntCorruptExceptionId\n"); + test_ThrowAnErrorThenEnterATryBlockFromWithinCatch_VerifyThisDoesntCorruptExceptionId(); + printf("test_ThrowAnErrorThenEnterATryBlockFromWithinCatch_VerifyThatEachExceptionIdIndependent\n"); + test_ThrowAnErrorThenEnterATryBlockFromWithinCatch_VerifyThatEachExceptionIdIndependent(); + printf("test_CanHaveMultipleTryBlocksInASingleFunction\n"); + test_CanHaveMultipleTryBlocksInASingleFunction(); + printf("test_CanHaveNestedTryBlocksInASingleFunction_ThrowInside\n"); + test_CanHaveNestedTryBlocksInASingleFunction_ThrowInside(); + printf("test_CanHaveNestedTryBlocksInASingleFunction_ThrowOutside\n"); + test_CanHaveNestedTryBlocksInASingleFunction_ThrowOutside(); + printf("test_AThrowWithoutATryCatchWillUseDefaultHandlerIfSpecified\n"); + test_AThrowWithoutATryCatchWillUseDefaultHandlerIfSpecified(); + printf("test_AThrowWithoutOutsideATryCatchWillUseDefaultHandlerEvenAfterTryCatch\n"); + test_AThrowWithoutOutsideATryCatchWillUseDefaultHandlerEvenAfterTryCatch(); + printf("test_AbilityToExitTryWithoutThrowingAnError\n"); + test_AbilityToExitTryWithoutThrowingAnError(); + printf("test_AbilityToExitTryWillOnlyExitOneLevel\n"); + test_AbilityToExitTryWillOnlyExitOneLevel(); + printf("tearDown\n"); + tearDown(); +} +#endif // BARE_TEST \ No newline at end of file diff --git a/test/support/CExceptionConfig.h b/test/support/CExceptionConfig.h index d4aee17..a1b0625 100644 --- a/test/support/CExceptionConfig.h +++ b/test/support/CExceptionConfig.h @@ -8,7 +8,14 @@ #ifndef _EXCEPTION_H #define _EXCEPTION_H +#ifndef BARE_TEST #include "unity.h" +#endif // BARE_TEST + +#ifdef BARE_TEST +#include +#define TEST_FAIL_MESSAGE(x) fprintf(stderr, "%s", x) +#endif // BARE_TEST extern volatile int TestingTheFallback; extern volatile int TestingTheFallbackId; diff --git a/waf b/waf new file mode 100644 index 0000000..29349fa --- /dev/null +++ b/waf @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# encoding: latin-1 +# Thomas Nagy, 2005-2018 +# SPDX-License-Identifier: BSD-3-Clause +""" +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +""" + +import os, sys, inspect + +VERSION="2.1.9" +REVISION="beba77c244731800bf15a003232e7040" +GIT="f59b4af3d1a4715e8fc8b5bb43e8acbfa390c9b2" +INSTALL='' +C1='#2' +C2='#.' +C3='#,' +cwd = os.getcwd() +join = os.path.join + + +WAF='waf' +def b(x): + return x +if sys.hexversion>0x300000f: + WAF='waf3' + def b(x): + return x.encode() + +def err(m): + print(('\033[91mError: %s\033[0m' % m)) + sys.exit(1) + +def unpack_wafdir(dir, src): + f = open(src,'rb') + c = 'corrupt archive (%d)' + while 1: + line = f.readline() + if not line: err('run waf-light from a folder containing waflib') + if line == b('#==>\n'): + txt = f.readline() + if not txt: err(c % 1) + if f.readline() != b('#<==\n'): err(c % 2) + break + if not txt: err(c % 3) + txt = txt[1:-1].replace(b(C1), b('\n')).replace(b(C2), b('\r')).replace(b(C3), b('\x00')) + + import shutil, tarfile + try: shutil.rmtree(dir) + except OSError: pass + try: + for x in ('Tools', 'extras'): + os.makedirs(join(dir, 'waflib', x)) + except OSError: + err("Cannot unpack waf lib into %s\nMove waf in a writable directory" % dir) + + os.chdir(dir) + tmp = 't.bz2' + t = open(tmp,'wb') + try: t.write(txt) + finally: t.close() + + try: + t = tarfile.open(tmp) + except: + try: + os.system('bunzip2 t.bz2') + t = tarfile.open('t') + tmp = 't' + except: + os.chdir(cwd) + try: shutil.rmtree(dir) + except OSError: pass + err("Waf cannot be unpacked, check that bzip2 support is present") + + try: + for x in t: + if hasattr(tarfile, 'data_filter'): + t.extract(x, filter='data') + else: + t.extract(x) + finally: + t.close() + + for x in ('Tools', 'extras'): + os.chmod(join('waflib',x), 493) + + if sys.hexversion<0x300000f: + sys.path = [join(dir, 'waflib')] + sys.path + import fixpy2 + fixpy2.fixdir(dir) + + os.remove(tmp) + os.chdir(cwd) + + try: dir = unicode(dir, 'mbcs') + except: pass + try: + from ctypes import windll + windll.kernel32.SetFileAttributesW(dir, 2) + except: + pass + +def test(dir): + try: + os.stat(join(dir, 'waflib')) + return os.path.abspath(dir) + except OSError: + pass + +def find_lib(): + src = os.path.abspath(inspect.getfile(inspect.getmodule(err))) + base, name = os.path.split(src) + + #devs use $WAFDIR + w=test(os.environ.get('WAFDIR', '')) + if w: return w + + #waf-light + if name.endswith('waf-light'): + w = test(base) + if w: return w + for dir in sys.path: + if test(dir): + return dir + err('waf-light requires waflib -> export WAFDIR=/folder') + + dirname = '%s-%s-%s' % (WAF, VERSION, REVISION) + for i in (INSTALL,'/usr','/usr/local','/opt'): + w = test(i + '/lib/' + dirname) + if w: return w + + #waf-local + dir = join(base, (sys.platform != 'win32' and '.' or '') + dirname) + w = test(dir) + if w: return w + + #unpack + unpack_wafdir(dir, src) + return dir + +wafdir = find_lib() +sys.path.insert(0, wafdir) + +if __name__ == '__main__': + + from waflib import Scripting + Scripting.waf_entry_point(cwd, VERSION, wafdir) + +#==> +#BZh91AY&SY:d`RM 8#,4u#,b3y}#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,Ԋi6݁vؖ¾7sN&VdF֗vsO:uՕ^_.p-繯{={JS].vmʕt hVPϡުwqӮ̓MѠѭ^v݇<=se;#,#,#,A7#,#G'}f=Qg/:vi6zd+@#,5\ww9zz@P#.#,z)]-#,#,P#,/w)E*I #,(]#,#2WKn@֗}hv{=^)EFشĔ}=^Gۨnw|x =^=u=I{ݽ{o{|i\<{V;nwzKnPz.[`**DPi{s]y/rm#.4]ۜa[2"Tvޔ#,UT ٢!@@[wBջ6]u.|O#2:$ZѭOwq@Usހ{^_x>޾/.zw4z/p_MQS":{mV=gCfn.K74sۍfokto2L [.IUץy{o:{`s=|rAPmwaof;mj#,^AZdl)#,:h(t:\g6uGuI7n6tW [kwkj1EzܳzbFO6ٯϕX3oG#2 ѽ}pJ9[o1w,uNvEshϝ/e[nh #,@#,ɦ14Mdd@#,O(%4A#,4&jO&T6y&oTڛhѐ#,#,#,#,#, !#,$&L4)LFʀz2#,#,#,#,#,#,)"#&5=j~OPeCAbh#,#,#,#,#,#,D#,@ !4=A2bCCA#,#,#,#,Q@@ATDSQژ7=O55<O&@@P#,>W-VZܭߵjiUa`P@D O}QiHx7teO7zkRa>_w$B(< |yq$և#2mTҧEEXLbgKod?>#$f͜jV̵Y6hƭ[i! @@ri!P1&KQR>`Pcm^˪جUj6}mZCI&"d$%F&fR(64IL Fjj+f)4Z!#2dQM4#,KbʛF-QFZi4Q bFPZ SbC,ƙ@,iRѩȚҵX ,fd̀AMJI63S-&&d U4h)#h@٤m,),640`؄!E#$D I2#$2ٖ$$H҄fE,34BcIdd ɶM(IdjHdiM0MJLb#.&M#2fLH-%D#BEfRHcEdlBb$X$d"I!&A6F$BHlH`J I)a0RhY-%IbID%(&-$Y- 1eE#fID٦EC $Q#0L4Đ)JZ4Y 1шJK$lhR"I&a hP‘PeF4SMMjBX1LfJI$!CQbQ+(Rc`#H$&)20"ŀC,%L̵J+DLȦZbQP4lLli54Lbff`2kd`K%4̂QjTd,"HQdR"(TRII4ѤlD4DcDLFXe"fQ&CL%6JS! 4Jjjl&1Y1***4)d(H0h5%EFM#LQ!ML)f)!BlHU0L53FS#HI4Ȉ6,&B6lL ɊBlD)DM"4jb 1Q1ƪ*6L4B$ѤVBl#dk,e"a RF1be+2ٚF%%-FR0abH)L eZFM[&YJ)c1cEi+ 6MejFmEk,Ii ڊRVUEIb(։%$U&4El2ѵ0-cP`%`2d+HFѴĬb" M[fBe3YVD#Md&ԵlbU,,JԳMRȚSTbєk6,4-CEYf,Pcd1"Ɔi(!1*(dC&I2dZD")DB+6`%(S%f#26LL`cdFYJ HhƌPЉ$ɨ*HĆIh(I$AeJJF0IBj-"ͨhٔhCHQe-a4jMa#(EbIEJI4daCbc3TlJRl4Kb "P La&Œ% jS1A!3XJSQ fɱhf"i#.#.L1LclF-M%$$I&RL-(QZ36I Сf61MFQK6,e$)œJRTbQhF-D# j I5F)HR)ILh5EMU"dIM)Jd d؈R-"MmDZEL&+i4EXJ)6"V`4eIRBcbIK$Hm& iMLE6ld$i M$RE+I,l4P2JԈFMF6ɨQBmIEE4&BLȈ͑5221QZMF5&6[DR-Dlld)CKlTmZLєeEcX٦#iR[(T#M&&P,Q-KlQTd)2i-ihƋHPC#, IJ6Ѷ-ikYi*hemBړDFQDFhCLPl5%76Y6$4B6l)/'j_Mܢθ˜DSܿb-3m!T4`:T?uaG_ XI5cq/U6ߙR8h Nq(# B1aU&P(Q.C~mm`?ZOv6URXպ%jWt3N-k0Gӻ5caD#.NG]cz޻6 #Y,O܈4Ֆ2+D&VlTƗ21TJ16LM9;\ JF;o_*]ܩ74H#.ZA(pLUA%wW󖯓|2>Nkג,/Ub?Qn^,zf;[E0Ŵ)U!%^潚ҁE7"r#2ptX]ж ̷}7s}N֗*qAu!ݩm(U{͎t.̮mҤ^;6*3]|EBT1_7u2~9&+EKW^.h>};D&Cnj,fُM;*IA, $ P0T\&ɐwe|-rth5ת#U^?zf9:G#2y&$&/z3(fV9wj˳ިSG=/s;wktPhU!#2Hsc*wέLHM(R#AW<,e)!Iե`xQJ+=1kbUrIj0)ChH*i~Ǎ&{A__ޔcs](ΊHCV1C|)|}'Kۡ=tF=8 fE/;|Z{gzCMFôTj17&77v] $=_:0 |Z2kAC~ݺ,M$jRbO^(5ʛq PI`3lfyPHk1i&w,Zݯ-׻QxwG\Ѳk|ﭼcPi-X,uNC5#.Iթ2SL UHT9s(0cfY$ A?[vPȭ]4o/.Gy˵.h tIC6;<*QLZR+}KZ.嗧u REF#2(SfZjASzw>U)vڵ 1}'YՅTV'VlG:*uB'~\>WMBG0m%6!B/w/Uw9171aěcAF4k( &aHқfcjY#E@׆])|襁S,U5#2j(Ywb>}%̋y}(pC#.i&lM2II.@10Cm}-lqC\^:dknFtf5aSdy:XD+w`aI-z>%8+uB;90_BE`y@CXlNZUjcFz ]o/SG p:a#Kv ./}̗0`vC#.ta#O&6;a~n07^n^[ApcRߌv\6ZڊQ#Υ%%6ʢܸI*@P7Y fq6X7!L^z1 1a֧m{a־x6C7c\OW]lߧB$"]ؼ31Dbu>/@&R"a>8Z`‘QDtckvmsWDR#2,e3Pov}V})>zk3+>_JGa$MvJըlWmJaચ#2sGlG#.m)y)ACҙ{\!~nєEĶ~nߥiP@^Tgmmr|5eZ'Xjt81q| \ c9ŞT&[߳]cAݠ?Gњ23lQHBF R0W~Z\ֆl6GH+<A4?Zu>tvvtot[]p>^OD+\fAu'Pi1 _ev'^̉|0<+re2Tfz3XR;6a>(V5(Ig^L")CJ m쪤<VKEZiҮRh䔤Ҏ`;f9դDU {q"kG⊳M(W- 5E(DdFJʀ\O'P#2}9s}T?0~9%^zVqIX-báߔv^+u+\o2A /l9|U"< \Ck3e 3hxBtRo>@64WޕcwD B$Ɵ4yAFoT-V/t"Y8RwC[n\V.ߥvyGLF/]b!B9+JMP19JaK7pG$mc_Ưi߷;֍3rb-8Ox]lPmt~NZʡ|?)wGXtK5RW{e` /yUpPOҎayzq+\Gkvki E U4ULh7ZzDS\cلR?#.k6zN7)!y}6D$.-:f>;K>ޥ68Ǣ"vĞ^lR| #,5Y1H#&q={Ob\+<Áx}ms0fxA+)1M"4[z>J0\or_U@hn1Ű{vQ-ܫP#5㶨0mj"ᇼ*1VoKHM-J1Х|e^MwS'2hdL:~~<<#3yώc_嘔d ^? TJҭNƯN7ݮ\CF#2 yB/$F |NR[iUWӦ׮~4S9qTjs[NL)\)YVoVCBJ-T^9(:S> QziRQn>څWh(Ʉ5C^҄i4W#,AB;}uYNHcX+yϷr74wM&`~s!uT>-3.do~=mz<8&mH%@ŊĘXE0{sSL^q՜K`WO\E46&Z+icm"&H18(бw"-x1pbG8bQ 6*hx~<>:Ar-Kۛb'ѥkoMױղNz3vxQC!遜ة4l1PG26g;&gWT$3YB7ldiɻW6|YdotAa?Ů9eF Rw##2Q˹k:ơTd9/`X*ZQD8)e9+|WW#,}o`w*`觙'B4.^(\zV=`b+^#. [6p|{Gvv PNd my#Q{=q GX ȯb(?=ā#&B<\&7Zmݮ$bŴ9."mMU=}J{THNuGӋgWO]OD2͘Y+c9TڃnļC43-,km W9EϾS;y񠁡;~$׽r6MS4Xt.jT⡥JVciJyBdtxaGFK¢Cݟ :!C'[ܪ'&jıT1O{d5S <82 :="6tGaǙG-M҄`N2݆OKF$93h$.'Uw|=M=/xm?FDݫt}3=]G#,Z&i5#\&<"x(h#2p{#2@GMe7Q;A|'(eb&DH3#8H$jκ}Jz9]p.4s©xְKy5a;t\PD풋,DqsnMcM=!;-b9 ꛄb/w GiNϤҜ4zCE4Ji8b碔,i|sf*%- &(uc!&7S m#,,Q[[8Lǒ{~gmG~nD<싼O ީmX,nv=l{UHW߻P/~~h8''~/>0 EG 9+0l˨ jC`dnD«qÖBl &߆q0&$A%z@rܤf8xy+K$H$xmG0(E'XszmEGWTtЈ~JA"f_VtZbvT)G"%r#.A:;n׼!^t4#,Y{#,Udh}_(Hm6@C(bs`jnjgP#,#,aGzI܈5CDRIt12|EQӡF埳ߍ":+h.ƽ]em`#,AtXGtmo"Y8NP (U.>\\bK3( :}$@@$(#.郭0 V^|=#.uß @3F1찓0*΃1|661p-~IV@׏Mr#.u#.sfFQn=/(4_ 7]fC7{ޢ|oL4Un$|im7o8]nw8ta~\9a6׻A$k_!d#Oz,`XTD&}*smg!\~g叩7-ɲQRV6%lN'RLza-ȥA%Y̰XBa*`I+!0PPi!x%ٽ4$K@#2X=ٸ7k A<0Rꆘ0*#!D RSYND,̣0xԫX&G&q8:<~n#.9F)]%Bsv&ioO;2qP&^ fĆP# %—s!#.Aё006C7᥵5|,-}uyݤ.hݬvfxQ&'5H_R4Aؤڜ퀱ZJȲ=ɋJ|?`k-rmX #.aa(%^f}NW\;RPk(Y )tc @ #2 zR$XwP,!"u7oa-Xc]Qx/vɓYAh8,@;#,8OlK܇;/>]]AFAzDh=v&rͤ&<2f,yV,tF%TR)/_]ބߺrT=1ML6s,.A^¾|0roK0,:x}(I% e|ۍ׶~{^8]BGW_r3Z!IBAh`9on̽Xv2%X!7`[*t8ξM_hZ#.ZuL &L '>L7DTzsvt9) wp}ov\xEpCꦆiCflOn71&"Gaׯ}^bŁ;Ig4ϔȒ 0&XEhQ3*ZuT TDY 3K0(0\tֱqۊ?d<Xt+F2E $f/=ͫ(N?6;#,ku̐"87fOOq`>tO0wv`#tk #A+e+PG`PB@@@(#,x#2).)q45d76Ə /xyp=P(çQ~h!U)nCQ-0BhkuGu ixxC7;a:2 2{Uk#2x DŽ3;YCǴ^ك52NLX_hmXxIE #2zd `֖; WH(%lL(R5:GɀcWge$J*v":K˰`vEjL;.c[mTAbJ ,x#o?vɷ.\K6;G 8ORj}#.4#ږ0FKg8'οsO_$n>WBÂwTL"RlV_#.&\pmi/*'D\l6`D0.wVDVb.L@2,* 62cyNXCR@pdm#.ъR(HF=R*#25a}и @m"sf8@b1-ZFv)HP BV,I! fC4LҬ#,+;FU4H0Hb.q˘{ߋDX,p`(cPjr55c@Εl4f#.kn3Ä gw0Dtal5ƛ6]6r%"IA\NE3CmFdRˬF7#.CFsS3xo>p1ݱHcG^6,X(\GgQ#,'Sp#.ÑJ9f˜>O48NwIO>\,>M+#2lhؼ51[n.,4=(v~0ϗM5FeS7iFY|8I 6y@`싷E?a@mJaʍ(v5V#.:衋WLģU<8Ќw){ln!^ +#+g#(UHnnsRH~#2nxj٘VƄ#.WeɕHQC-(bqxk.i >nnG1|%U±F"~8cc0m0pM8lů/Lq`#.#,Bh5ٶíiHtpm|o]7#2=6!_Sjf OkFĖ&[Ҥ\MhUin=z++4W(ղ,&@, 2PkIہu2x:@\ >J(>y=< o·٘N^#,}!ʼ;sr@ч5WCz#./HY]PcA`h3]v8{PTPy UwZ=W 9F05#^5@&lIV*E3kaCy W+4)_<Ή-cSEH/^pjQ>+C3>[v8RHBgE3jK&1*X_bhb^۵-H!%J>jg #,]G,u,0zv͠b(pg}2!*$HS#2X5ϖt+(NO:?O/~4::##29bn,#,SG#,XQ"HTvN{4mklIc2Pc$.jC،?_󟢏;nm6#E,OD!o2/=*[D|h޽֟y0TׄTثn77{V2?u?/CK @FdB\zlY}>3 YpsN$ [CDIۛJ^θSce<he#>-vYi4A_?jZAD,jqr~UH~rC`vz"eέ~!CE\3!µсbE/!؀gqux&cHrx&V*P #2!' #,K!yPz)/AFُ>7ڿ?W# }_W,zΙS=0-OgH)#.``_ʃZۊutVx~Zor:\j퇏tl7uhm^KnO z"28U@ϖI*/Ƙ1@ydfE/V%kS6y/yĐB"|e6RisUn'Vy l'ߎ|!#2Bc1nDTJ<#2d}ô6,?xǯOt _Z^d2Ls=;E7@w#.EgL}3c(Ui}0:{=#.F/STb+#Qv#.R5xwm5X*}EiiU&uw]>a5izWi#Z_{ so}Lr;/b=vZv}hĕT'y5]obfPH! #.x4AM܏w_&_ǷCz#.  0jV%w(#LiPL$och TQ_#,);F3trJ!=#.7-H:fa;OWC">@7I@AH$%EE2B.N鿻GQq?M$k@#2)\ ~A1}u-#.3/(!EEϗ6:;xkjy*m뒪gջ]e~6e/1ɴ̓z5?%[]8isvxzE4<^etm={w`+=G `vAv=?BF-#,#2r%.gM?Wl8{uzOT7-m#2.7gV鞞m[x!ӚD~Hr AlϾ+ۛώΛ E_}M}OT!,d9#uq2׾Qǝ}ۧ7VkF7[ӒsJ;ƩrJ-{3>V[tyb/`n^ϘLxvNo\skpk6YfQ= #.SežZx0#.49~1.`sssK9Q >nͷwMW;uNJwYM4<tCåc!E-:=vG\RE%b$h|4ضu:\oc*X^ivl9ѶN#3<]GC.Fߒr*ffbpNqyFi}duL:>2]O/ H g郴YS~~o_`+l#2(H~j6+k՘6Ӈڞ_Fzwvg9g ǻ-;|)ӸJLEG#U{ڷ#2փ#,b.L5grkGW|LOL|M-t+Ygua*ssoWG 2*s(?#.#,:7F}ʶv[GF}g>h^8}=,~g2y[?(kZ^plu?! {E@\]].#2z4郗#2$T!|lϛb#.EPv.^V?8AEQ[QϷS#.,Tw'9߿N˯Gp37ᷰw]c[~[we ffɲIBԉAx_4{?vEw{Phhv:iTA+Hpџ85zWhp8|95n޻QTm&{6}^_.I!y29>)|x{*ÿj iKxA?B&x_P#.0 ~9aXsț'#.dWy-b; }<մ=Չ2h\5xj f)R":0> :A*$5q@w({v @?&پ<5Z67@o&a==n;›ߎ Ok8:A/N|ZZdf|&"7w-uYMy[Lto3Y}u}ʍ>u҃`_]l~fKֲSZRvoTm=)3'^4%fHKTKuhOٍ; e*6x>qG\S]}/S/0#2s 3}wm]y,N黚Eގ0=NԒnubM'g)q=ҐA#7#2 %{s$1OÂ|-K:/?xǬ~@tGmZci|u2gxᡓww-Ff\ږ7a+,qG mR8"<RIb4CCX1UPqh,BCj̳5IFv@-AȱaHH5dM#.#.Ѕ$IVzc Tu0N #2J噣n͊ $О G N?WoG|t#X~E3dCx_ CMWUSԅaȣI㠔L~LM0(v(m#3Yވ1 6J0SC+a[*Cc4e$I#ց0\cJg(eԊ&`IF)lZ>36YZ8!ZW~M<.E[m9W^lZ_զ]!>_l×-+ f#,-g$9oC8KbҖf!d6<6)w[F#.AIeCN"G;F͐I7L>Z܃b֪I%UUQ8$ExhM`=-I[j?ӯ"N)mcM9+b,Nm X>Queo:Zm4k6 Fأ,-*~UQ`ۼ[]"#R:Ed%|tUdNG#"DLdcu"t{M$;pﴱdqDf52;S4haSF7q!76sRIBuޖwziH#.ʈw=]~OPy@y}8H-^$1/y%^ڰ(C ob}&aɠrҵ2:`GI#2s,d=4R?wˑ9Nj0 FimgN?6a$ifU`E#p#i+c\0A:7)vaDžF23Bxֺjk"pj8Z8%1ଋJ,{wy&"1!`#2THRST4X*0EX>d4#C3|%c7&.Y׮-4ν(>qs "Vl۞wCdA(᧲1^n#,7#.nyxXdѮ ,x}#2b1!w6o$#2-7mГ$$1mmRlю#Zy tũt#.,lp[&4:i*yZeTP3#x8\F Ta8VC N4ݻw9i˧Nʈbr!Gcw0]jK;븣}:K%^d/Nƛ.wE.ظV}Ň#,@jftWuų"x1MKNLeNƦY!cP6Yr謲aSa͉$g%sY+ݰ,+N6ml4|QXZC" f%dgb X靷ݛbRq78=!(n?tؼPRZۥ=*!ȸZ}' zzmL-ȞTφC`FJ8זm[t?˝6{Tq9 wki#2b-PTM_]ۦf#04,ŝz-vE]{r"c\iih1frU.:XZKdN). 6g:bsZ4|_0:p7vB8¸[%X xstQg9脕m9,L k7!(o//q`R8Jɣ<0Yעv;L.AkUS@0cKi{X%`3Ƥ}jݷwJUjh艤4UM4tekkoÁvb$Ћ8ҤtX.8f<w!z3pϺiiw&fW6:4l귉ŭj"7Bt)Ô&L+#,BrE;ȰiDz3\ޔkYn1AUeĮ7[PDNvUf#,=]=UUp+F#.]kh1YB}LQ}'HsS޼xF=#2<+>? > {.3U(}5naШ ÙeGOa~Gwߵڵk7 <~;5@eF5hԋA,G0'+T ,W<ٗSOx#2$H`eP ";EΟ7+w|ݚ 2瀹i߱sIy2᠖#,$r3,f1Cuק>s))^P ]hRٹC 8'?R]qL09G},3|$#.Ğ ѭ}~bqq`!bL\e)+P$ϱxgs7OlCw "pG@934]s0s̛|%ُjИh:9~b)Ju%">:"dT-nLfi=@"GIUyiy>+ ^A\@vٺ8`20e?#.E)*ٖS˗!B#26%X~[vkdŰ#-fRb\!Qys-˪VlLDHwJmBDDLupk0RhziUHiaM8V-&0={Jk$$.2I.QDq[.&A钒cwańm꘎&`qF}'쮼η~Y[H(φ%~UȡO.5)+JM[hy j)ͧ&uGfq_pZI240f|9c%{{h.ؖCp 5Ǻ:Go{P00A\dI-:bp,~WĎm&6NJ 6UՅ`xf-0%u{^~MEBkxaS9o3'm^B[X{xlOR1Ch~ݘ6:&"X0^^'!pjx~SP@錨;"}:#g|8F_w.zٚO;NtLEEJ,Irmʹh, !T~h==5P`O?F$Dm9>|ya`ti>PVc.9jA1yΧ>dY# sRC"l4$]B+\h`91p= ;j4y xZ6p_BOio)XR"S~sM$BO7^UK蝳P;!V>huZI̾\3)Ә}79rN/O#.tڈ}ALWg}m<0/ƹxy=)~q0;_>]D6Lm4gK Z~|T\-^$6V[ᰓķ+'g4N1#.czt/u.W.3 nF#tl(PhqlM6'9U1w%H>^ҭr'<3M!ƯPg;-=WBhUzFE(#2%mǞ!ʖN'癊K6%#%wٳ+evvd$Gd].4dԫh#ޛIz#ï;ui7vtH{pKuɘL"駉n"||͊; $ÿi4Z>NvQzd`2Fj eE F!Xf:E1dfFIhu]S[Y^IA]9]ޢz?D2:X_oр p3X1DZeAv #2*3˅Kf9qU.h^#Z< z_UƻcUtB<鈯lXK|0P003wz+{)J=m2dIn[LWy~*Q*VM&=./pZFAi+2ɦy3Ͱt8] VfcⒻ8V8zޅxszwώϫvLs^5[+{!B;D_٣,͈W>Nͼ0=MRGx`:6Vf&:%_AԽN$dbGTg KenʐBKYdh.P0v~#.>2\]swi{#.1BlF')xyq{>cI'¶lg]5vu] !hQu#.eph&I01}XYGPkc'H?ָC³ 1( \ϻNŚBb]`7#,#.C(?fL)^s#,n!t&юY9#283^]*K:71@xg;2gh.aS;PB쭂^$tDHtI;@5N۩}ѣq3|ҳz\1_ն`igIGUx_J#2o%oVaY売?8= 0~2Sfgu#,V"j1^lh#.f1ݬpa-K¸`bEѺ)C䆝_ʤN~g oXuObX[8<,}u^v>]"ut9Qa_S|𑷲h}Ez}2v. ICsvA:#c2Z;VWGdhJtݾ:o*es} ˧]GEho:-YhMKW^E$#2ot.ΑDVJV#2E#.aÁXCvQC9;^o+׭b]B,G-oi1\UX2#.Ens?Dp:.^>/rYg@zD*]#iQI;r:qUUxtG$#2sedeUgO/\)R@@#tQ5@Gcsŭg );qb#.yDo6(݄-R|n0+JɠIpV B#2=+PD΢8Q%#2[uaIY) Y9{_X^1S6vw]N\1#.{*^#JN\[cd]n) g{$~\%<_ӌqbvYCFV#.t%±w9OrwE#2Ys7hsn®Z/,#e2ūng.|n'xʸT*{9`tz!q6BLN T⴬-0S0<9k3l#E- ugMNjvڤr~\nT{Mtw͇w;/d;xaYQ[>J0 ]AeDoۤ2wsP j3|<~@^^[J{ߏkgjwzἜvJwhpXwm"m#2`rC ڍ[֡M|u;!$+8n(Lo=~>ZH-?$tZ$LKgY8iV ֿ,&WeW9LC3duYa&W(Yz=x=:ݥ^^27vJ={{oJXqëjL¢WRVF&Rk#.YÔ/ba;c~ӷ9Mg"Vw覱`MXWxڧ/&%*uŒaL<uJm^kSҳN`t-3UWBSyh5ʕct_D>YkaɆsZއ aF(z#.'Q9"\t]L ;!_Wc/ s!Bc#.`z.]$ufXEe^{KΆB6Q9/tFSFL0Cyh x \v+5aŬK CJ K C{# G+#20Pd24Vt3f,]ȂQٔ:ANХ',$f1tms&( r[JIw>@}T?װE1:n.f0ƹ@]"/#2ֽxvgš&.lIˬyXgZOH\xc3GY' +JMAq\ J-s@L2S=.mA?L['6\U"ٖ]5GtWVS\/1bZte$gE`\#Lkxk?WܦyJG#2E[ъ<}<}[g='Q͊]fʶA5`÷;X #.N'nEks:)ͽ:KpO^W5xe.q64p[J݇ M1~;%m#,t@u~t"m/[~t#2!V -"> &oLcI>h:^XapSu߾7 &H!us8hv"R)Ƀ̮~߷dZ"Z!Xϊs=*!/}0pt:8z{M{[|b:MU Jd#2E3G;TP@H} 4l50D  ."-#.|YPss!HӐ#{Uf6pv|xmNSEb1#|՞Y4Fn|>2Rr\`L35tOL/fwUk/5r#RM 8%j7#2#.q&vس5 /MPy70Lspes8s@ˮxs=QsӪ"@ QBL; kFdsW^+u"#2D#,N&JW_](?ve,eء('qBI _kh^o,$6t`A4dpNhYU{n =d)rrؼn{ć?9¼}]hU諷)kCrsy%1 X1 `0[ d˸Q`[9ַ<;#_%?9úVbh`/#,(5)Zzsjw3ۿ.{^d#iL:4,ERP#,S(O˨R+`7> AD~}Y&Egʮ]FK8CqD)rؒ|ߓ͕hEaF@AB#.=΃WcH n8[-13ڿ33LunYH$P`#,&ׇz:wL?l#.8y֙k$Yz 3G#,f`}EaR#,x~Ϙ%XA5F6bUʦEPA%qn8~5?$@Po(͡mI{p&WE:{RDQ )qnlNy b 9C#¢޺fף-Q`{ WWеO]ĘZF5jJv .zXfp'E=nHS]TafZqtVs#. C?qw ߵhmzfM3g1P";ǩL!}) xq}ine#28ˆD4tw)O>#.Шd?-ACC"H "*yD2`f,#,>qaIO-kSDH]ԁ[@;!`{@`f," 5:k٥˩#2O4'g|߼#.ThϭE`#dBӡotgǭ8XE($q5޺7`yf>йLgML ,VUv{fP#2ADfXAi%zw%fUbƌ7OLe{į Ha+ IE~?G$; Fi!G#,C#.sq'W*7%j kZh EDn8 G̪l1^^ꤞR#2dUM7;6(ܩ:4`ļZ^!ژ L sOiV8e3[m8{H`\ LlAAp(-(W~d/!ּǑgnAp Z^L#,_[cԬvQҌn߮{TYN&g, /m'(Ee#"V¶XmDzrr^{*B0Vd̏KG8IC4#.@8T#,(HpP3ރ;䤦E[\ktYbGym6!b$t2]^;GLpc#2#Яq#.9$"Zm#2@CeHC 6OYa[ë!#,%-D_|;q!rp^o{OsãF2e?`bL.607tG/Ʊ"v]W^>G7ؠt5ώ\NeP1GHMͤ<A%7Y+ЌWЇ O.R}â71 (mz5Gm>XY$G#2BcO}Srڟvvj:sϫN-if#2l;r#,\>`gˍh <[z6wܾ߾ ^`Ga^ٖ{zT7t:pK@QbQ)xAK'yC:cٟ@/pn~P\M|35(ԛp܈K\| rq'0;*鄧SCfJ#N)uv9rk[9pPnFp3}Fkp׌1x:,4R0x%x3c[ܴ[盯Uxg_+5Ş'MCTնhgb#.nFH]\bEUx4G\_U?" +|Y'L2𠪧4tĝ^ۣ׸!O~}xb/!E:PqC —&'eL#,甸H<ۧĎ@Ȁ弤g1#,W/51L)0Nq(O_Np;"&/Ҍ|j$zt޷K?|\ukb|DN:Kq&<;} \*p;H#.]F#.?a! v'orF:jb2f{<Ep1M %bzޙzO׸/R穆 #.%#.ڋ}R?r|^uӅ5\-4IꊂcSqMd)QݙC Xp{˔l,_ _g?J›q S@6C:*l6[CR!_j0;ˎ@C!lt1Tj"*2P,9`s\PCXjՋ&YD-P5AumP@#2\Ꮺ#23)$rDȀ{K* aw8&C#.)@T=;ɘd AE@m#9ī`3cD3WPk`Mdӹ<fD]چܱj͹U%kj[bܵsj-ڹk۝5쪙%zwҪ#.&#E&a7:'Ȓw辈` tpC;Åj43743n2$f˧o8Y׿_UJ=cʃ"_RѠEb4bP<6#.x>%|\2?~Ku69H*dR,\ѐG6KT$2xII y8:(9(Sε:#2x`.flg~]\;Rp#.1}tm5 hk{[򯀨Pk_ tQ5bp*{hvcDUkó5e(mSFC52P Ѿ,,P^t ]5:}&>;糚1Xy(V:t[L`Ä4˯ew^\w2Jt\cɋ3ή҅%$ I#,XTỞZw9߫f$26;7R=#,d 5bf^KU Pi% ,Ј oGJn(J+"##.q혘On(P;&/AoFϚ/#.o|!#2%s <Р0bѕ*a#LH0EVꗿ|#OעG#,y5+!ꭶ`~ijAt&&ww>rp4!Hy[Bx>j%}U$ "! 2-hpXd]#2r7n2M@C :qS|\t:d{6>LqVBn!2I&aהvOƘ.i{C:097McS緝52JK?<K@Ҙ $}Z2%4D<< 4H#.TqVFX-y٘> 7|=oCmvv[eTmH 0"44@@Pᷔ+%MybB#2ChV=y}pe\$ns˸cu*54GjⒼna=c`Vw}NӢA83ǂ[ slS,Y#.F! _?_lH(Um/਄"^㈀Ƃ\5О' 7;S,,by&YwѾah$â;)\= 1G-fT əTlO0Ζ1qzo|o]J:S tZ9Iobew-{fPks]V^9o|mm?zgdש Ϝ@Xn`|-ݗnDy]P2'eqGɃb}q:_@}dñXbBa@olך~xbQ7 61 :P6HCA39x78 *FZywe Hy,YpΤ[qE5ʤHA$}{{3CrN)#2C =#.ߴ7κ9dwCלԀ{{^z`L1ivӖ4m9ssm܊0cűv~&]*{8YTQ(R Pp~`iAP˵Q̂0~NN|O#.ƽن5P񚥌:%00 ̈́i`4Dwe$YSuD&9@x#p#28T$#,A1&ty#Kp%ֆUZrm"HK6!ɢn!gd[]jfi6jIX^upۮ Hٚ;{*~'[RՒDHTkC9 {&DÏna#ry6[#,x#@4C^4=BO#2 @9Ε_Y2".*U!QMKu: r(,:bLuURW#.nA0NsԻXw*:|8O[L~#.y3"1*e8ӷᒺyA˳]?vU?B`bPyD}w1H #,>B:g`611@K=v_(c@N0UM"an#. %uNa7Niπ#2}{~9QK;E^zU&]cŔk2C01&>j K 'vn2uV?6OaH**H,#.QjbIW?w7tN^;8R=s+ $dL1g,9@#2f#2 w~#,V6i ~q\"_??&z{{^xڦuQ2a}V[\s vzOa V#W_~eK#,3K?+V䘁XOژs OFD`**DD"]hOh7GTĐO;xʻ*x\mT>Q;뉑3yO ÒhÊ3)c=*Pbɫ>c.ĪXD^B(:No6o?e^ 1}~.DʀlNۨrttKplmZAŇt^%StVug`gQЋbY]rmJ-WZ6>p+?vL>2JȘbOƁ wzR%>T)_˔6y72>^4@_?;#,=Pw*KD0f! (HOCqO]m#ܸ"i򆩌UCwO?O;A~ݝ6;?>cw@kB{_; vqfTP&j#, ;60ٳ<ޏ*Zf!꽆gvs"" ACߥak 4DLP2ȣ>>ZP#.;5Xl m /BsS#.&&= A!)f٦Fz\^UzY˛F.w>.կRX<>ܚ/1Q~7#,#OMvpy:pvV=Fgօ-T'"~kSs`cǿȿ_c# !] ~]b QdՁ&> yjN?fa뛵G~rslCpuqUt'Ȼן?P2q˗(|#,oWa=Y %}ZuT\-{֔:UG5Dr%(}#2jԸ[YCKʔڼ,f_3a2Nq;>[ˉChv+"]s뗧N"KdfП|UKb"e2"ۤ<#2MKDMud*5#2&+*(KC h(t&w%Z/zav7A?xOxuqUק?ʧT#a}HPj<ς3uwnO8& -`WYwa|DUzk1Zb(<:3rKLl~Zw)UWGTcɩ|rNs@kW>ɁphIܪ+w.]TȱҬ߫UjQJ:H_k#.a#Hȓij)ttq,k Jty_$F;4ĸSW>;Dz˙C!R{@;E( %`!d >=/kkr%lIW4ջoѺzwxrACC:t!qģ+~6yluXp3[I"On|^Ӻ-t%nNc*1Zc.kƲpl90BZģڜ))?hwr+o&P"^#.TsivQU:%ڟA&1EvrX=W++X\\bFvV,䨅>5i(xz)I<܉7vB”+2q}^ #,]wN)a32yx\\ȣo5-x2Lo{Y9K:D) sjԬ >YH5`=kMht\0;T\IVabaGn*<\AVAҊqF>EȦ"/rdќ*V ">mtEA_=M|;A^6eqkMk29cSӇ\rj|J-%C~w_&`֒XwKdfP=¨dPj'W^zA#,R8fZ3IgG~>"-s(2WkSͅՙZexug;鸳PD}6wi(p K}\}O֘z)ҊЅ7Ėwdj).ΜId]z"pgTw+e>TH^~Џl-~W^C2Ywd glwpXG ~Ksg kOg2?axh.W#2T>-h10UIt_pq-rݕWBc8}V|vj&Qg 5g4J}j)C~.gow2lj6 l H5@%EQΡgj%Dyc6g I=}4`m}>|19]H0{vIb.qW:'h2,3>ߕ`t>jh1߇[:qL-^ԫ5[|^E8_~M@_"v=S5g tCS=}2Q؅YE3#,جRog:/?/r>Ϋ홃@IEeIsf-sᤸ $ $iO@]$Ci]Lb@VKĘ^RT*uL" p"(*ZZۿOSkmA{X@xX#.~)\,}"XOo s&ޗ¦b:>!ʮ5J1xĐj o8$ _V> #,\tg=x,^?ҾϯV^_*; 2r[ξ[g9́(#2r?OLOOUrP{<#.db0?)p籟z'oBvAPS0Wjk]{jy#2t%O{Osm8< P/h3X_CfXP._#._f>}]ۻ. <rmZRK ֧0#,2sDnсq80}jڿ"٘k#,9U]T:C~G%~y.)Px2#.J^A k˧ 9fHzᙥ'n?lj3~:B*#2z{5)-5~)%Q4߆ g2?*˰ mr‚_lsk&Q7Rlh$#,1MbKSa`>W;9CT)X$ߣOI<](zCog%R#.(PV``R8O)p(]}v|;'6*ƤT ㎉Gir/#.!6qeDig7l6VOݘe;gUTH"NGPPjB3nQ05>RT=cF0;~;%i;4.Z4$ yܥA~#20)0Do|j.OE:O5p\#,-ې*E̳yo\JD#2Ufd0=u}UKϿP4jWU#,&zT#h<9)g\Hct CTݶ>)Λ̶[6Rr2ʌPX{._fD#.H.,謷|}9~n.Ѡ#.).|qwTiߣlᠭ򗽈aE>D|s~ʪ=%޺xa.)bȳ68̊pA#.Vg="7PA.V;^)۳bC$u[n" d:㡧ԱQ-轜 -i&itʻqlkjܼ+5sOm;\8gN"4Fc#,p(J5rT⹽rl&)WLXto/E$; V=҄d󄫧GoI1\OnI3WeX2‹$hjnɨb$0[ +r=f[Y%")apͱW#,tU /#.ͷ9kn8h!Lʈ}NJ):߾0E_Fnx~!#.zOF#2ưpc` ZDz?Q,R2jP.%Rԡ#.bl7&7sI0>$|Cq:Hns{ZCkNir䩞B2ntaBs2!0g#.O\t@LZ7߼kvrv94>" [gnLKm%M[{<ݘoN\v4` &4Ilxݯ$-G~AdZ^=[3[_- "<ӫ<+\rmL49-5 #2yzyGn}2gCk;>ɪ(r@|lۻK}sB*qpߠ^o"n{?gz>,RT#wo]F (#,g[$ga-]6HDNG#.ը#ϭx\j(:x/xZq7ldQ"D 7=8ғNP.=bDŹuXd!s %kÝ#u=Z[O@fѣ7a#cX;I֮[مڌ-Cxh԰; t-' ]<3̀h-eꖇ#.vHD#,5\w|JS ;Ky%dj$(1`6EDʉ')ډLLf7j;A=֫Bz±һVW l6L2)gxx`Eg`scx=EkxFH#2p9ysS46DpےZGǖ+ve7;Fvrvqƨ,:"75wE#{tPЂΉؑc8Q5E(+>XD}[ 9PE[tH%V5Q=@ӪqMPSa7%&-645Rbp8Ll:_Cy %>s.@@Juˀyܩl[n!EdGCcMNeaOv;tkh/|kf}ugAݜfϴÜdUH7{h!#,Bɳ"I#,L  U2gގɑIbKDB'v#+6RV#.'Q@m#,9GN#.aԈ2ht *قMߙmQδhǔ8]T.k#^1ƘË*9CZOeXHChY֑KTȋ)gIl؋KdثH^m\P%‚B#2'˹g^\T? 7"#VFlj8{-MR? {Ú .}Vesɍ#,|!ϻjB@4?VYI,ߡ/g\`KG;e <[1_9_'3@9_yӃ"IZG1gc?ӁUU}Ä{o.͔#,aT@pCv2#,s1 }E@'#2B" zRJ R*aR3UP]K Er|H騻4ED#2X5 .r9vQ} #.7w]2r\C4tM#,~wM$6p"eݻW9}T1ۄX*{)Wd#. c"sm(ل`K]^V1+#,w<.svEj=NoPV>O2D90a/#2f~gגdl(j$"׵z/P v'O\.,tb ~մv f Y!nm[ޱ^W3 KhFۥK__s:`#.v[d}Ly~N^#.mP(XmTDsUs_?D=Czt0<5inA) l8#MJ478hieP.4<>CoO\jјX K+e#,\9l59N"b`ӘZf"`oB`%'#2aiN2xq#2)cՕ5[,rOg/CzDzw`s^']x35[JRnG yóG}Y " ذRAI Yar.^+JQl E8Bs\#.r$sV6GakhaXS :1+ x~4@:Oz/W;d2Ĥj_> !}!&gŕS%|Tl'XߣX8 kQ+]h0C*5:ؔ@ˇ6ȐCR,¦;!Դ2n[>[gctZiY~6dO@Iڜ#2JVWJ0>SQf#񻝼$2Vp,.BވN9aeeڥQA#2i5/;?6:$#274ͩ%%nErv8 qAF#=4Ɖ(`*Dy0Pd^bO?hXmk-TE^'q Wd$6w>o돸X,ITϔڛZh+(c[wVj91Jg8M%lAUʏz\[Be$!6|ԃ|^3xLi,*t$XLӰ̚y44 #̑P0OVֶ֑jf=L1oR[VF #21 l+i{zWmFw~."glcfx#nZSc 4;̟EכQۑ"J&#,=G #2.yfFŎ,0y̠m@!Ȍ.A#Ĵ?1B[6]X ~ǘ=#.46w؈ZjH LQlЪVY$ rPRm82d?ۍc94,HoWy];rJk;t CH%#2:#.;֛oPWsi?!ʶѪ#2j8ڦ}+ܘ?]mKUD=<["QRR"2o!;w[[ fd@00}ig:LO6wn!#.#.#,8Ψ*ba5K! uJd(?̣mxj"`/Y0t ܮZ4 S#,_#2a糎#,<~HA#,'goM?bW"[!.R~0RHI>vEXKz޵JD@Sfb'fݼr0.yݎ)=9#,=C`,!ߘBD"H>~vτ>E9?3>ڠye4[(@e }ŐEАE!pdt¥@,J4x~8C^C{5 Ke$حO$=O'B{bH#.d>؏HӬT+CN&Gzj#8˱\U(A"(uQ8 hw`qhi5 r;|_dR0]w4#2`y*"4IjM5 98!۷SK4%2vƵ#2k. f1ID 8xhу0-G- %8gW7&)D^,0dIYAm*ُڇSp`Jm('wDj]6U+wj@t# vo9i%^i4P!lǁUJ/s~IP[ak `SS"KMpiF#.U3UyT+qĆÔ5]L#.*ECبҥB5Q%VӫzN;g`9DEi| tR$jaѹq*{@l4 /9r9}߁s(x)1cD#.}eE  vҝ u8u'a^W#>A1/CMSt~!O…)BPpVGֲX@o8)'$ l0Qh_fҍ#2y4lNtmCn:#,b9#. k#2=vXSvUX]+2\b\2R*nO={'(.4w}1Rks0i?|LSB=-HJQEDaxУвO ErӾwC3f!@#2yf KHQAhW)#,#,mB9R.(g2"D6lR#,`Fj1z{v!TQ]!x!pJ|{=90#204~drHS,a]Wܑ'#C@ +#Igk=!+ btla#/f d4#ጻ"=#',1"&BO(Х_6U(E:ѵQA0>zɩ]Y_hP9) (]BR~w%ǁE #,Ͼ'=t"mWza#,Av|50LWCϰ劝A{'SդeSF(#2IAAGP j#.)zn!.bNs{r.X``@w|o2]ܟCܪI7q8UP!h}|pSREg'E')2ѭ-Sc#26ęD#шL$&A=qCLO)u[z S_JL;l-hǞTkP=`ʥ0*dqVTBSm Jh6 6 1sA`˧:Anm%]\|P_FfhˀΤ$#,Ap:c]"(FiV }űާ6#,@8Z8o a,LT\_k|]Km>8yʲ*y">|.hm !_yh960^qn딸,5a8D]'[r]msTsM#,V82@ H* !cksPذ)on=бDI`#.1!VB :RkwԽ1ˇܢEd2xPc_"XT^vO>˾0,bhAC#2ݹ4knauRq1*$*bS09Vڇm6R;BP] #,P@Pv%T'bQ\- ¾^H{u|1Kܝ}`d%)# 6{>F(wsO(/- oO܀3p:pz(j5$ ^S!n$?;{Fx9KU۴Jk$"#,\9:F(u~~ coa+@ #2dQLƑ#22|qkp $I-G\뀟#J7g$J1\{z}<<]7ՉJ.#,M Ȫ-m*[{k14 &*L_LlL~a̔pߺE$19|s ˼ ~6ưt*#2 ,#,P(be#2J`Iv˃)j$4/uc-U0_nLnQ=tu n wPHMa_(K`4vf#2A} qXy3}Zbo3.͡ƅ&eS[#.Q/F2$ˣ =mMo Bf9f_~&h ~Lf@o|&vj-:YB<t0ia񃋆լiLqfșzg5^Vp}2.\n." LgˬnŒ#yMFdsۮ|N:q7L\(nm;VaM+xv9;b#2\&5=imށ֪srţkz|r/]۰h`(S4h-|zml]0?T:b[>X% ah8m!8/#`ao#,#, K4GA[v<=Hy6>,E9UhqU;r95"]?V;(s~'WspFzò_͑ιgKHI$0eCF}#.޾e?~k0Ev05T@`P( s)#,X@K}26_f-~K ~1U;kDZ?6x׽v5ONVG~(vD\%vmQ[UmL?t)DHP"{#2h?cj3観iiHIv aȀ}M;sB#,)agWf?RBrP,(0г#,B RǨpÅ!*vm<:J۾wcsʯ#2MBqb,|%+b]6[J;KE3`Np6ʋ9^}:lf =52W#.$; 9GRXEo탦,g.Hv`9a$`tsMZ$\X!ަQyت{otac$DbHyꘫqڲd`2֗#2ä9#,*MA|{ @aYOq~QK= ~-hD}z𶦡iޢ;CT:ffNqT#,|m2 lڤ?eO?~rY?lL993-yHNY|+#"(6EYݼxћGY۠Q{ab6i )R1B#CjNỼ);'VX#.p:si0L@ɶvnWw>pb#,ĴMw&q[Ms{ˑh#.;/A FRHEJ 5¡f9qb)e$qz/\h6Sjl)aT}KxW!g,)p'QFoy&DtZv4jz/\CaOaEvDm{wVh6e70pfAB 8Ji$bA4##2Q3K@Ep0v+ f"%^wyê^~KF) sC '>˪-!0$X.ad''#2wBGo:N7{G1_TFMM]{~l_)EHv{ԐXIXmZp1ɖTD͢Aj2c-*U@iE!]&DX#2lk+"O7b H8IEJXX?_?N#2̯灂<Ң"ݷfЁ_p HcApAh`w6v!6?jL,ɰ*% vۃ=|#,Q C%"Hnp18 #2Õ2({@{).1#2;/U&{9yb#GQb]ZT4(a\:|$}9<#,gqbol&,`D0֓JIaӺ۽!BsĠrln`##2˕Sh%wp/qߌse͖&qzXUZTq3eu>h`첯CR=[Ȥλ=v0ZL'Y|T)2xuם:xÓIOܗ3:[1iR6KE&xq4.PXv^'Ֆ-oB8#DvOG5{arŒF;gHvWg0qSېԅ#,i=9vIb:R{S!Rq\3rX3pM!#|Ei6#,lGڦ ZșaT))UTl:lPѪ^;}N`" 㙭B*-QxzG#,6#݀cbg/lNXk#2i4`#71 TyWfHƽ2IzڽJ75ؘݨ%/G]snT\(9\UAZ"mCD`Th͢#.YS P5IR#.lշ}n0SDyeRN:&#A]MҗqTzH4#.BB]YP=Jc+RtI"n-P;@-bv*hR[`v,dTP1K;olwP#,12%b_#2hzN5opOފA6i2[xzݻ%;Y#2rY)CU8,Y~͡Yh #2UDuJᤝʗ=7tȒQ#2(M,b<>ӯbx`R&=EtNX4NEN aG 2$t{:v"QھdžeaGurS^uuPdB'bu~MC*%#.ZCyh0w`} ц\C!`)D#.RL2lַ? F}؜x6'q֜i(©T'ϊ٬8`HMtrO9)LV7#}"1ߟC]Y `UT*eM~^ڭm=گ_9-Νڢj(y`|毪Ȃ#.Ic߾,;# } B&P P"|2f>|밲pc#.P#.fMwjY,CMUm# $ 5 2c@#ÅB\xe7s#Q>W oc#.d`ОjBS0XRyB<3#.\Q#2O̼7.z9ĭ{cKEmR#,Re5GJe\msֺĕ6x^."*IӼq#Gsz1&ziuM6bY2{4Un(/li>;Sbg":`"iYcKX4)-a9¹G{X($juam׎#,Ƀ0!#,ܵOnN#,[$HBq–BDi@T;#2#hAٲjAHJۀ_K3D(x*:^>;Cs#,#2#,5/NJloSMu#.6iW1<Đhl3fm6D͚۴m/mCaB_"ޮ! 0G3jGMY{nvպ ^,IRhDieZt8l^Á!"3V lzrWqm#.MzvE#2YAsyjTAv02%y=B|HXfa.O="21 33S͝y-K#sۧ I" 2Xa8GW3ׁNys3ZšC!iKN=a gxse̍0.Ž~.r:kq(|< F#2*ǧN=fm"<#.y$N|$ߝl՛eɛq8C~<,(,u{k6'guP sRh(HZY[].lEgu-u,RA6 #2(Zq#.CQ(#Tb +bK݄V#,8NN+hBTA($&Nx(–Hi@,YǺT귊Cl]v'f#2D\lB_rh >D |BIma#2@";N#,C [{خ#2#2AT@@ CZAep4l5㪵maXkZ\Mr#,PPQ#,~[6QO<:<'zAy!>Xף,c@- 0%D(s:1A#29ar$DܓGC>ɦ#*#mf+iiim)cFغTUMW{p ɗɡsNm=6j0#2!Q܄ F #[35ܹŲ+O hI@r63x`Il;aT ?W'%]!߈tu#.N8_te3!H#Py9tj~ujugCrXb5Yj6C^BV_4xaF8[~*B3{@]k[$NYK' ۃI"AڶoIt-LC BӅԥ+\d*kWNle42N&*U~֤3sSHv7T-nM6Qxfl5%[ b#.)KG#.<#.3Pa#,BBP ā,9ͮqa>4Dgr#. $A@$̬LMMo.#*H)F%diCB&(@48O805 'c,)l(ho0iinme6mo+k]}s(1v0}/ ^Ѝb{ܩ3ײ*:VMeRMrP4ۏJ#,'g8v`Bm ;Xtn{XTG'zgW>!AH@WLO5-u#2y %˵$a4N,+c^<g5@y$g+kʮ29 K8ykGءrut5#./]=$$& қ4,~yDu8ЉnQ@B? 2b#,;8;ZHK<ᜱ*Y:aIcF~ x뛕+Y }RȜ[.*1" ^ bQ$:h<"Y16z-W'Ql8#206*$c#,ȥ#2O#2WIsx)AxhJ @!LB,wΎ]A#.-VNu#t`^[ɠFBO֑$#lhя0!Mn0d`Pu1D)5%zRBBi&zx␛Uh#J3#.HྊE;lL"Tcɴf1W"Jz80d(e'_gɡPa|c),ٺ/LjxÝ+52"1f,cTX0lNi?#C j TCs;ll!KqXtf#.p1(ʽU9ZkGԄ4O<שMF-fd)cIf90"ꗬւ.qE`v7#a%ӥL XqvvSZ[撳:NjDӊ /^$I.0ѧ6r@^u4};ѤuŮa!*wwsؿ7I(Z,[ȦwS[[v)rV]]vLp_+K73#W$L%)P&O/*@w )[#,lbHki6mh`L#.j4(m !L$d#2IbFLOLJq%#,Е%A6ZLS&yw+1W+4E>aa̱Rx˷{;jdi/PJH(1+9ڮZ{Ѷzc5AD/aDy$8O@mBþSnKS(fi]NKB;o:ZIߧQv|zF87|}==j| K*l(bs{iI#,`â(ЖAs/KUJ%abYV#cW#.̀7d.9gɦC1JB`#,y7G(Hb->#߷a$݅SzLFo$3f6z mG?z96L[U)f%Db}ӴSjJpb~&K3.+&)_h $Pt첄4E='$[_t:4VmZ[W+nmHPл.'"pzNmq#2`#ZlBHFFIA)2J*d0bTZL C*&2RJkK cI(3VhS&&i")FU2JP;K,)4dIE0Z Ҍ#Flgtɭ1Mb&Q4,5R"#.4fLƒ)'*vh|(RhnB@g~'|B81P#.a 2lQ֪p=qM"vD%wwϚusrFhI#,ܘο#,̣~t,fZΫiQNivqD3p[sZ;GxqX۝|'hg 󭪧kT[iqojNfJ]ѭ>  ZU_ZVڐ(ilR1/Þ1q7PGүcUkrd: L|d^]w^rߒ2.6a1nh5\1 g *T`䇽5)*Y|'unt篸 x-^& a$='&}=ҡfiAFq1/xeA6#9nX#24z6K)(-uozog"]h'7:ZN|wԜ#,1>;v>_^; ?z_|t؍'s~2y集~'vn\p#@F$#2îi Gcԟe}{ dKUΆ0Ljt%'Q O #2;>9u6B@F+-#,NQKF#,6y25#2{cjYRzu'h֘W܌qfeֆٓU(kPFN]t3#.s썘Ny:r1 1tR2!Т3F;k{8@<5iuɘSLkou'˭>d/n6v_ahD4`S*g&eTYgX*Ovdy)JuCt}j#.#{v6{P#Ix3Fӕ3f dgfx-֒,px;!궒4,T\T&._gvb#2T\6(社JhgTU6$UQґ`"!CI1sé4#.f1a|֎&"ŚcmvSw :!A텷k@KZ|'Q #."3]F0~[}G#ly-27<6٬l*C4fkI.2Ry^j#.mGyxĚN(%k|c5ɛ|8$gC#,o"50P3<އ&fmOQ~t%%١H\XFv·31OR35v-%(qW_7n5 ̽^P8Z}-g@Yr;#Eiy'ދl Lc֠an[tzr>Sa4B,Qah7jҹ#.4 ݈ R,hmpD͑"ZvDZA(2(‹lc0iSW(m^mK Gw+8g!oh›ђzX9!2-!VCZ#.y钰u˹IgNw]8k'[Lp36S4Kcxe1B4׌)fF#.2\bp̦׆WA4\6%CӶ6ѽQBfă-N0qH^N.O: ò5Rʡi֓ !W#2 qIQ ̾[pD& MrإDaakZ9[>#.PЛ.#2J%S"ML>?#ڳMlJQJaB>s!]398IR#h&Y4ox>8xl.\$d7K5l\;`#Ԛ3F2k`s5 ֙dYnjήMX\:,>q›g}CM9G_#,fS@324N;-&KL&UBB͔KOwj&57-gmyqo|LWŨ2[Ce4L>P{%i>gq_r lĉ\)MaɮɼۉHf{Lua&G֗ H6V O-f%[0X#vfwP YgkCCױ3t#.MŴ?P5n.pd.P,Iw!CuZ]n8ӹjkLFED"X( "PUx[L#RHЩM) c\8(ĥoxb'z\o?$UTA#,#2pb!59uww6f5̹nRȈ#2\*) *[?),(?Ą@)w#2;7*rdrx}CPj(f#28Oy{#,}k㜜 ?P.6W݁p̋#lH{.q[нDF$[!!a8vĔo@Pon F14hƃ"EzU)Q֑)GBf؍J;G&CEy% h?&b"S򷏈\b^zdјVxr>g#, #ض6ƍF֋h1h6 2ѨI!H>#瑚p֧#.]bvSNAA0~p,Y1-F5R$C)QAƐ0 8f!CL3‹_S<! ED_#2r>8a$@!۾;#2 #Ki-[(c[GR(ĕzxyrݬrU|X3X@4+vLhٔxr[ޞu-7'epڀw(Z/<.ĖFDVAd#m&#.%U)nwC Ԑ*]<nM($ɽF\JDd7aPLBrR5΍%fv9[6XhHCr"dMN$ǩ" TB&1#HlS,׉@=ol@610#R~b k%Ed~lE `¶$?#,}b :vL( Ҡ|_&$ ó[G `o\Umu?oxB`2]y|tzqYXSѲ'N+d/&ѥ*\RYY34! t-v5,Qh&#.pjTdat^&mD۬ ERID%:kxJo$fmNy@)6x*GftÐl5rF-N72D3kJ# 4P7Bc #.JwOX6QLLasY FSq mٟv؈4!䍽ɩ Xf\f8Lն>bxLa1ںE#.kD/zk6{QN?{NRP`F410I#.aHRQlZfT5ٳ*ᰁG9>wjKJJCUbPZ1sm[4c,B2aD,ͱ)ChՑ ۛ(=H>\%wqC%ٮwWyΕ2Fe+`#.%f5tKukWuw]&Zvn̫Q1en#.FKF bS6ִji.6yݷ2˲$,ț-5")kKM%R*ji}עj5h)$ *ā\̣(+Dd7/tL 2#2p8.Ģ%(#,A0,#,<;(=xzFCz~K8H|#,q(@ fX N3C(>;3 &k ] ##2-k#.pj2Hp>h4MM>P?8r00Q "%@.HVTŔ@b$ؘHm9q\RQ#2H6P# ENa 98n2;3#,XQpȮz$ (`P:: ~~|MDxa@j.\*;h6'K3 ׉+,!3`ZI!愁d8i)vs6Cd%Gk.h :bɮW$YK8BecF >P|и:cqyȗMxx(|?1U"(}u>lod8YG p#2~* #2)x얣j ?QVMx݅%*%$YC*%fL[{Tie-b"e2SliYg]aI5YbjmTQzqEd[S]MK]Y۷cF" mf&1WJ^dQmUHFmmn]RSTR睼Ji%f3UefogjQTG5-W^wɡuf+j]Q1*y]l/?v5fxwƒXߖrU*x?{vEuw-w^%c಍8P^#2(ӢtYLb"0^5!%=ZZm,ҚBk"-JJ׺\ձYJmwZ򖮚sb[]v^jlRgMtR#,#,t RQdֲ٨RiiĶBZY-Jmٳ6m4TRVUi(%4B#2fͲ&#.3F(6"hmeţ,-cRmjR,YJ)LKIڊٴ,h*LS$&#TEK)ZLemfdRқlm-^wjV[iZ[kmt[SKZɭZ6M\ֽf6UEJ5AT4'qvVt׆)H#2ވ^mjfogD0zNG32#.X#.N{$1<C)QDz#2'ߙ_#,Vy#,{0'5=f]8|yqM7bb q4{L`pg}sִ;vEJWit!!5Ij(E@Yt"nlGv#.0)\%|=]& JW^} R%#,###2|AGn&:Ӕ*h@ٜlq̍qܽ/7cI"{Y! HyMJٻurR0&qCh$YOԏ̬KF Si(7aߨQjc*LX*/} SRʷP#2>fqم ٳGg_'ch$T \.nnũMFmi$I`BTcTnbrCp yw#,#,#2Ŷ5T`|뤥ҹjH@R0A '\\ͽ[3T3r04" d[#2("|T<%Nlՠ'05Th&④T`S iD,ZkSaO)3E@wjô=z_q_Hn>@#BƕFAX[5VsgI/Dyعݭ':8c_G6 D6U) "g¹w}hԱPכrHB^oTKI#tiv&t&fX#.nS ԖlئT >j_Pw܈4XBpìGV>PvUh96C$AXP$S >4b$-v|\zA PcBeK'$I{|>7.*XwiWVH"%'=DVM@*,rɔ8#.>v7 %>b:00.Pȵ`=G"3U#.6q7#2 ]N#23ERBdY&Ȝ08$rtu>4AV1Y\WKT#2"@Ɓ4מ²3>#BJ!kj,ݬJ&2@AYaזDq$#2#,C&AYRxfHE<@F`ԑ#.3''8OoLfDhEL&FMNGNUMZP;MF"o#2M#.4ҋLFE;bev7Zr#Fė#,f9NX=$;xa2tk8I̩-וak, Rel-&!WSgrzKd=;Mgn5=PlQhՅsv*,?Tej5pMgql 6 sQ#˥r+4Wv"B) #2Di5#.[zɡ1ڨoF'X5Fp%vBչG HP1o;}S#.㼚ĵE.4d,qk<%#2a1|+LL28H90ma`2h"No߆#,#.Q W0L2op^2Υ?yxQ8=ԞC/ӧE\zwqy!Bf5ՈNs}AÏDĄ# #,le5E6 C_ˀgD$IWTR*EUEJ#,HFŘI."((*%!b bmP D-e)EpxI'q:;GߵylV!$H jzSڔfu4-c"odA!-~u^1ӕW!qV/k}Y%)Tbbrp.X8e,0}ƥܘ.Vw'mnJKQqyGbTII:fIi+לem(JQH 5Z%4t`cZILTU#25=4hF X!, ZBСaAcoƙ\*ea)"$ H$a@.UtkDm+sݰHa2 t5$FhlUdlm.תobu޲ytMjyGh+Ccbn|ʞv-F׻#. , YKM)(0"0ނ)-$"ۑ{^ZWklHX1#eb#ŨƖكZeIHX!Bdo%#.*°aZ,M`IN[h:IViQ˻s6+Q\1&7Ro8R[(6dDU%"$mar#.$ ƭ:c1'Fu"'Ya#M#.jƎCH\؇oD28& Z++STSQؔG*c6a*CKmyԘ\ՎBUKϺF0 h[uvtEM:벻mh5k4X% ?~X=%0tn'.^G5#.ܒA!#,u#D{BFdSRfi%TacXv]KcV1jinɩ׶Z£nZ$A$IJwh#2q,;4"-1!#,?3̰bRN=c8Ջ#% -AIBAXabqn"T`m@p6h"Q" yᄊV><9\Z@!A),k$hl^A(),-njYkƼkzm}#."(J4#.#,61"Pb1lF]\ڻKim]i$RVԙdj#,mia$@B( 8+.Hu4Cɦl&A]C8-D*%]m5#2\Q>n ) $ UpDq#*W{qH:@#.ʟ=lF i 7I{$7RwyBUHǟR;H,JF.2j3Q#N]E?_>fai#,#,EC>Q7%\:,թ|{ǁIk1KJ=qIFZRMM5Y&ݶ#. *0'#.v<. ZwF7J+" I!>ߩ}dBHaep1.lw֡#0U=#.Qb=qf^&\0YP(tƳDJdI 2kX]QbiLHj"&#.#,(rmoH 4-%zC=/_|dHh( q7+:}nBPˇdW#"V2E#. -EKD0_x#.G=#.#2>3jm&75DpMYǍGKW!tb5)͘hL#.F@$+RM6a0 1aB؉ IrwlV2 gq׍|M~6Ńէ&w69τ?%&$nd☣j.;sec*#.:ASMtpb Y;"؊ZM3mJ%d̍{H$JvZY5[m6clR#.,ImlMK35AYDU6͵&ͱ-"AP4?KJʘAHQ?Yu-(E8ԢDZ2#,SE]M6ݛ*0mM^ѼښNhF-A*R#2@ 3$""?1#,?|A;RB4eQaH:FL@O=#;eX+#yNx`)rQ#,{bxjsCh~ d{C$ H@(u D#,j%EnE q`/g+B/1|xc&.k7#,e\CB>(Ρ5N@ȱ͜x C0?B-pSOx CH#Ld$^!G֪(4M~ұx( A@8]ƻmlݦqwɩ.IG5whzDB+XI#.#.Gژ(E#2D!Eq`.\y>}#2h^P̶[qN1f ɐeSC&Lm7H$Qm㖻0kO^mSY#k}JTΟnC}/@RMXY Ae% lBY)OvCMu!^o!&+/X߷7uBE?S &@RaBhcM{@EnJq#.0#.YT`bLU)yb`HE#CB=; WdD^4,ea$0(^$K h&cBk-/"l)P!=J$#,Q,H™  C7?uH"hȁL^ P40";O-rI`#.J^s462i!F9/#,`=gSЂ#.-6.4\*緷V0dMTKb4#.K)) ^-{JDz</Ɨ;v*:ϧ3HvT(;c"S+{MbGȉUDH$"`HEZ{"# ڌÚ6`ޘp%Y ?,~gA< 6;t9Y)NEMt!ц+#2/s@->CuHzC$() B,'At:Mҙ) } d2ڊ EќEC_* h!t 5v`.fMP* c#..)UhVal!?>*,C&X##.%g}W)JT˸$&,GaT(Q? "xrk.X!9t*O_1•ElgP/ۛ;Q2e1kI?#2:$4U~um-yM%VRdlj~_n~/ZFw-UKKwO`hLvCk5d* r)N=G.;񷳐#,fiW[cFdL!R#.t1㫅:rxѓ~K" /wsQ@>HgL mg%`CP8-z 7(2)ak#&XS7{34S,Jj >E#.|^jn6<(sӹ-Way~oI9_IqkE0,|#2P_n9F#,V_p:RwAP<{!d%]1^bSp9|:g:kWH8VX5{G?"YM]PJc$#.t>2f ctݨy(ÊdjP&P!/jff6Xnnd3վʉctD=uĆ8XE"Ո#2ϵ1{*[JBA-Z^e[~ o hB&2 "g"7-jF,DUpDq"Dxɩ+,E 8@nlPI(jp LU`HK4,HȀQAp*#. p'PlH()x@C5 zΎUMǁA풊 UYZŭ[Ĺ,8O4zxiÃzv>#.44ZnLc=jX!$#2L4Okh: ӄny -V_^m{I7ˊtr^oC>\M%АA%% dflDtL#,pHDnNҊ(MF6۫ښZ5YզdkmlXS//.o,RRPdDd;lzaNک18dm?15cv6+ 2 5ZC 7Dž`e&#.p}3jtPo}xY!"T*2;98~b]<8` tI GX9 MC thzr:K!ev HW2#IcÕnEsw!(:|#.eGqyhF<˛R-SPJ:-!1$yMΓƾW}vTbF򙵧Hg-A<< I@RqU}UD]-NJ 3"#2'wo9oCl> ̩2wA>=#2Cg$CL#,.օD& k/xB.5349\!r&c#.5iEM/d 8$H Nyvt\Ny[LaV?KFs<f,nqc<tԖDr|ÙG55K:&̎p͆pQ'H_"rdfM.׊`@_#,lf;#.ސ 츣,Q0#QH#1Da0gzmDƖ+a~X#.1iT[ٌ`cKlP}HAYgJrlGO<bb3ن=ld3GnZ*\ʇ{bziG>> F#,ꂩ^*!lr.G%H"9i׶kmhyj実bǍcjɬjѴZ5m]R[F4m`]kƫEUNܕ9+e#2D(RꃃrX]BAvm8Mݿv #,8ϬMӺE Q&=p`ye)h1">$@H#.50\oujF+c̝^!BM|yo8dg;ad\40oq)iS$k S: #24"('Mu5= `vN3x` ʩKT*)<K=yz3 FB⻝voALൈ_K>Sa{婸z!.#.z='d|9?3[TKDL%t̄r93C;P[r]u!#,`_; &kPUQŏu&]S?է1wT#.pz =6&;bz0W#2޺7_^=5r#.#,#.pOFjNor(H#.:A*몝uR6fZ,R #2*P,a}c`qYEanh cEQ.#.E5dAת֌Q K i#.i[1# 3`1NPnmme+\D2,*4&FyuQ#2A]pq/z3L{nf亗hYHZHTfn# ,XbAS[)Y+I$Xҩ&F,ۛnZ1m+yw4Rf7-뽞fi#yq"#%Xu&FZkf"#-իB DQ4kŬcۣPۺvL5Lq0Ϊ50m#.܊$qy&i@f⛑Mp0!#.#,eaFSC%B)I#2׭(kܬ8iAQycXbG:LͯJ*vIDD5i@-6b{!ZZYX؛i y6`QH1ᅸУJh5XCL1dƆ301fs7! l.2ASf†fD;+(&sZ.kXy6fSsà!b )@ɷ,G;#,87i_{uh^KfE#.Hl32=P }{iya~ \7Xcf:WGjo' +)d1!fiKi*lK7ơ4◍jCuUjlP-RGi(^Z&vɰV=gc&r #pN$VQS-6q'Rz!AUnsp>,/ăϡ*cDD@*#.iRL1 "b #i`HQ#.4(JB("'bzѷumӺiFNiX#22e#,ҴZB]w.t4TwS!K#{frj\z7x;]uّ !B.R[G:D$幙ru7WlmL#d.a{:kEoƆX4!S(ቘV,В*o1E8ù!6稹PhOFm#0vAL($L~ppy)k(}YגnxnyppqYa|$S΍)ŕ4hm҉!q3D3Ados/;Ox8Mހ-kfl#2놓l*52b=zYq0#.Fޑh ͮ۬Ȋ01K\#,Fȱ~T#.1P\ftԹV%Tn54;MFU+nԚxXL^M]ۻts_$)նImEa*IEK/z0;yz-z?7>eت@2r,Y pH ̲.[I$DQ&Ld(B],n;zqKj⺦G0B`*^#2uX rOlFЦ,a7Dɵe>-3Z4gm-"0N2L4Ļi#2N҅c,{|BAܗ8/@3%qH&6g-lƇLUfQS(VuReM@{/<.4 8~QAVdPKogU)&鷿ګVOo$ܱJF"/]hᗿS h\~y`!"Em~VoCf/#,E@(I$QQ)I"  ,`ur`̡C B ƕ" UL'b`' $\[(&(mm\Ҩ;&w\D]"65N,=yv&Ԧim)@qq&.+VѶjԥ8%[h"#2(chPaBTBB i/Dbf Mp$#2X7Kdnl`~F@' Y,(ӓrzx$?ESa`sp%Xgdͺlۿߚ9bpc׃t Y~1< #,ȕ^BAME4mMD1*L8c#53l}ɄИ-bi(6l]Q#25C̥hT`$b@#,4V1aFjW$x$#,5^9룯I%|CxjKF썶oF{#.45A!g< zO(.v[[-WP#2F!(nСkw"* jf[W۪հ2{1Wyl_dd$YNY2#.FF7 +((ħ72qY`K%#aM6E)n(x Y>z}^;^`V(7/Iӡ=UWӭ#kQ跪=! TYQr錂+e:ܬaAXASJ /.Mrݹ֧;wRF7vשky,߇&j#.BXq V z #,9guW)I"eĢ2$3`.ƪ]IHXצ[ԖdTJAij\("8\4:n!: )L(20*@$ 1(3=:] -TjղͥmS,A# O.oY#,rV3hEHH1ET$ ^ŌbuV_RRjeL̊?S#,O*H` x g}mLO)CU}QFFtBP$z}ܯ:ͽ<<\9a,B|!!Iƾ'1ΖZ-EZQ1A$9hMFGVlӊI81:!:%ob!ԑg['qh<]R.5%,eHBVYhDS )j]@P()D. %"2FB" Rb+Z$%DRmьb*3UV#2o[?#2nle$0 ̩#~͊/8n8xT;~ H \0h6<.SL$yi?.QC&3ETHc6Phم`!Q@1mEշvˠD ~8q3Cebb ԫIv`\ Ǝ4gBB5uB7yԺXޫf"  { XL6ݢ%GY& t;Sz=w7pԑG#@T,@X#2%B րRZo5eviKj`C!AgV$bA3nv 2N WJjp֮o FT-7$n =?N(ʂ/k*Q{LCfk6v,oIHc#j06i1;k #.LѴM(S@#,J>,OXUڥc}vz)--.ދX >a(ț;9sm0l7-+CnA#X36ƒި^ZS5h!šfg.7u٥#.#49e%2;I#,07-E#,QT64V~ B<],(s?"_Ȑ M'V,LĤ(vDBpguAs+9FcrT`0#UNo09!wVN&A|sѴmV㜏_ܚ>9taAhD.7N{+$[S#.0m;ѪnZm=M}S!cQf}w翃5Ac)!'4ogmµz=iٰ y/v3|S|#ؒѼ-5dі܏s`(8kc.һ1B!@6RabBPCy,oW.MCpފPUz=/i0UhMq"(6Tny*~8EuBo~#^#lL#,Β'a^ |o4kpF:-^z. ZExc`4zSTn!#z(#2kT&Oi#.B>{716ɜFڲܯ4D(JU>.yeA@f٭FUUUUgL]bi@j|%*bᐅ"BK#$0) .Ֆe\{Y_{')ߍWfl*'(sEnT8QAiIeͪLk9Fق:ckO&P{ g,ўA2#2B#2 ivL! _Jj fZTe,9"i<^Zh x)I#2 dgSw;"j lKtL53lw(%5=Ru:qzFczW:QJ z1`zu2I/v\'羀k3t~~l*}%#.%1"/ȌH E%_zߚUEIj5T;loR}=)̍ԘƔR@MpbG|i>60-< sE zNf\:[r>#,qpP"X mAZ;-Ĵd&2[! (:T;öET4t&]|G)#2#2=vM}=9ە(;gC5a-#2XDi>{T(!@B#,( c0β{78$GPf% B#,(AfVROm?"zAFgI#,>"4U";XA!4}_G\d79+=N=~)PB-%EELͨi$[Fֽ CIuB0:bU<"1zr0qwH̸.}v+s<Ļ_hY Jpw H3c#,195`#ܛbm$&C#,dJ"81Mt H#߳T+%}gL#2U1D̉|#,MTQ9(ݑGT#.uetU"#.cv&IYէzؘjjQu=[1t)M#.jH!}#,Xg˥$8xn_J抍q^!&#,Q6W-uMUKfHFR 0R#,sg9Z$9K#.>22Q=TCR#,X "x26O}dmeI$kݙe (i˦4J1HIdaQ1RsCXX'!Ϊgo<%˚aY964 dT#IMk Y" Θ~`~x)km>ce0,R,0l;,Jw+y񜟗wFw4#.2#2#LTjM~L6g,uʲi4 Ϫeq6vL7SpamEayNDd`x%:pbl]U!42Bro9niTݙrРn?'to5I" d_|S;^2*RinD  YrKrTS@#(dTU4#.fM1RA5;ۊW5k\#2k%Ջ$c]-,V@q¶i2#H"/+tJ%4m8bL)Wē(n͹&#.t$*ԉ(rBjd f"+xQhZZA{=8d\3FTZeF^0 $!s4S#2\p#.de,S4`7a, P9)Tqa(hj#.:M[ˊKb7wo,Ce(#тN:jiYZ2w(lCng%* -&mvwƸJ !}lW6d$Y$m3.I+ y% #.j7p>k%q3m:nzP5EIvm$b~F[ #2N&Q(H 1bIYp<#,#.hFu{<"ł%\ً o9Z|vGȦ%2w#iٕ?}c\{NOe]nءDL` nYͽqϪeO`Av҇Tj  W؜9yQB1C%h_d#,v[=Tn`]Mk9O>J\jt `$jwD_@&ԂnA$ETQ$mpcdcG"O*F#Yꬰ΃#.5#Ih2+i#.6#,rY;56I$9 j0RrvV^h7k%kkv%wvkQ,rҶ6Ʈm%[ecr-TkQTPbo##2mJb{\;`OOHk<2՞Lc &rzLPE!7PH "!*qx{̂Z0woUdJNB=p*#2P>W.?SȈtCh0#.X2 ąTv=9֋0$Z뻎m:-}\Z`6@DA6ڬ{hVIPZ@D骷,*RbT]6Բ&ؓF@D?i|,.ґĀ_!ғбs#,}S:)J҃H∠/cNDASmJźxԁ·2bDVGkݮ߱Ve5 $PFZ4QXR3-)"6*mZ&ҴRѩ+,d=UQXf@4\e6ڌH7$y-Dpp(5^4(L @B*䂌yL@(`6A`V;hƤtE#2A BEYX -P!#,/Y|BA}9\,oo^%O]tԮٍí?JzweUֈ#,Dh m8]T6TUF6Vt%}U-R{$>!@B~ݍg;8Pš;mPMԬ}a%mQ6k{K7R5NYwiA!`42PeQ#,<SIZtۇx7]ԩLn\[lV-rT!<0ډw?*cIe+"}jgƪ.fviu#.-x j2}L8vdq5uX\[sެ#2L)F{~l݉wdt7Z[;U}[l>-wNIMOG$$pG#.#ETͧqlÞ.Qz.\ !$mG9%+횏 b%qPrg['8zs:}:%Nxm@=ӕ,J&*dy2WKTakVFN$ZqGI$=V ?28vokpmGÉ%t<#.5vK8hmLĮyG,;}%vԖ_zt1Q!]5ʡVS]3B|%ڗvbAs XɌ)FndBjAnq!|3dQ;8Xf[^+b.Ԍ4cAn>Рn@/#I<7R{P@L#q$b6nNꉽ3qOW:3r0Di#,bm*t+@ P\B߿(J-K7Tb^[9'8B%Bi-#2բ#.TsN}nH]+#2U0QG69v5\0ct?Jʨ22JTa#2C]#AǶm=P9W}HJ%#|@C(Qf|q-ysS1 dvxwu%55#.HZc\/3Ju|Ҏ ;,J5"ukd\bNBƟ놢(w4>#v9F~von`qqdžd]wCW^<.uVVӥKqmM(Chkhph`'(p &W^δx1J|ʨٻۧ|ݳ[KG~6S>dջ7Ve'pNZEuieD]ww䡶,JX!{Q 9x.Ux=-oKMQj@́0Í݆==mK2*-O۱!?nQldLQ`Ì@@<>9CC0h\ffС.lˀ;lh<+8sw5;0ڡ~X#\a y eR5WDhkG{Jo#ʤj=iDBBB5LPmb\VQIFe3LMMnNap4MP#,\BOÎ`Wj&p!oVD]>0w6cR%ņ? VA:CKyP3|c(#211("фZExh1n#.`9e1M#sDpMX[2]L5c3m>+Wcxw 2*sKZ:t+1hT H<j fV'٭mִ"##, hĐŀaI QKX7Yk,c`μqR2q `ٖ5U2D>'ssRU \l`nxF#2gЅ,U$bŔ'@Ar9K2#2%D 7BhkyFЌ!)YSTV3cZ#,LegJ $UUT@aOSMx6OqeӦ3~g"M$68AcО#,WXRBGsV&@ R.K(6Z\Y3osշFn;y9;1cUSC-I`㴮8)!TCzSTc6ސop)psH?,@]ɹa'7@)LzC@²"ڦSj떱WMm#.#2*`CY'G8/*^W>^R(#,4[aF61F&1H̡jV)%V)4UVMSf1h!BjF$ҒTF4#Q)EhMMSFI0 6a$FI $Py9i\̔#u]Sz{3GJ󹘟r5O"HHbHpa+;*V*#,`sQiTcqI#e5bD7F3% 6L}^^2Gq Ha5۸,Hz.E;JBoY#,S8BY7^I$.[nP~ޝщz4q ۨ/_WV6K]am;C3-[Vj؉-&"lhk]5hTZR6MDQ,:#.FXGؖȲ  3;A!52Bۨfe0[)ϰ#2 #.#26##.O.V4C8C:b0pd)G)1ms%kuZj-DC#q*KI%hg ;w̷wQ3oa^Ro4Kg848(.@,]B,!hJ,hDJJE#,7<|,嶺̟#2#.FpȰ93rAn.qP Qb75H5#)!J#;CS8.L6lG:f6r1JN2,=0ybq)Lc@`% eU,х#2*#,( \@p AIpKy2HI$pQP=@LL)\2ni民+yE{;HVgoW=Q<Ն:741~7q8q ꖻc@.|kl%`# Tԕ &PS}^%!#,>Shbd=>>|^˔ī2]g匪(~Tmz֠)Ipي/z"2`:VK#, `-L$AG.|XXY\\\f66tDespHhEzmH^A; 䜆%:yQxV笐#2"[#.oO s#oMn`GKfG*(5+F1b"6"cvRB #2s#,:ܓăն[B}\ 77V< ! (=&(ݻ0\@y-jUh$U0i^Ŀ75A;Qr\Ic!}Ee+t3H4Rb 9(ùIi-VH@tl#,V@rO*}>%{lO0-g*0>bQWIq坔o^3]@ on(Ynp" ÄL皚j"e0:]Nn3"[/*f I٠QH&XpK[x#2 #.$cN tJ(7fH.17YHQ8`TDŔH.E7Wm]$:% T ^Ju&*RSqs*-Q\fPˑьpءM rlsgTqXA[Rt$ \aI|aA66I:ojpXN|e(͆t{OAlѦYF=#.;;-#2t9&+LFe%oݑ=̙,u`LCpÃ78ڃdԟ<6Et#)٨5WM.$3 hSN%ɝɑެPaʹtѩ" ݯ|Y:N7"q$[vB։^FL[3gWuBA2dѮټ ^I:rX3W5kr]3&;8k\@r2n&='NJ'RˍnjaVj&dKZ{w&^;Kfv~E0Vlh9+vv&?g{w#2뜣_Nr.HHXj7f$m:75)@4qt9)Bù3`ppN(7 " at-pCQG+f6[Hم0lh SYl(#2J#z+fh$Ҵ2h/gfT#.tHIm0;eW\k+ll6haDbu,y@x+4~ 8w*8KjZ.!mqMB\ca4k)վN[ua6~>D=\˃ȥk_&O2qҶ4#. vvϣN9R ;k7MiX | y)MN엠)SFsH0^m~"aud֠l5-X&0Fk p`Z=u&ר7᥌ XJA@m#2#.f =Kk"m<@mk)&]YxyzP4IZF6~8p,풵W0lo-#,-hb$Ɇq0΂SQ4Zq`0剉.5ʠ#2[lz1ەk;&\ &,z&yєuP)v zx#21,H%ta yeǒ^ݓM7%8qX tTȘ;Kla.:T)fB Q JN3Yl)D.(кQdƃBC_HIj(D{ ha+#.i6;86e9VCdg9ksuAd:%.$ 0CnF6"#.!q6&17SBm;\2hPa `t"N߅Tfb.!5oKG#,m@ p\Q*rQK#2JDI&.x\b:!Зz2 *Qv p.JtANF&Pu JZRUˢsCb<!1`SF4xO X[u#2u#,86#,j`6P&.0XYM#,`![Fk`HsrbQP#.#.2h&&tH҆ث$[, ! R,F#2|>U@QQ`2Wv3]ȑ6yI!Ñ$$EB*ȯ᾽ |O#'y=^j!ZfqACZ0#2n`aMrzci1m;Zm:#2C6kϴ,0mb'(6}JlW%UE`!Du"#2pS_m`vh'ck4Aὥ<UpwmEh;˕"pp2dUtuI;U;rSέLo{vvXوȹF*$ƿ$EGF<8?&5l܇_|@t7`lUC#,,.J:{Nwye&0*vZ0b#i`#c#2m&PwKy6oRe1䐄nYm!BZ\TnPl j!ҲI!#,&쎲Li#2-7CBx h1ECf,3H!;_wC <5-!= y'|a d}aNMZmlhz#P# N3#.fdD4(#.oV!FآЄpJo" ^Ǵ 5sfN2׽!*SҙVA8Oz@'&XFDݖT$`wvܶlOF!qI,̷ܚ*nqw,7 h"qGƞ)pDz B+ʣh䟎b8Ӥ ըB1*7I'87U("#&&hu$PuTTY=#E$GZJ#2&E&q5 <-PEUjwSXXXpr2_ ~. 炆!7suQU-#2=M#xscnĤ#2SuYmqFn&Pu5Y#,Xb$-4#,*@qM_i/Y _ٗ>4{bN(~o[5ͲjIͳIձYl&č5+ZJV6-6Fok~dKB-\qrC^QQAn9hqԊzعm#,HL_V z;5FqOjh>qTĈ#,φZ􄄌Ǝ{(MH#V2F暁5`J9;SmiZT:1R#.iL 54n(6DG˹,O((5p®8#VRmfAD&-3o:9$B-X &t2#¾+#299\FAeKJBXrdL#.G{8>(D_@2K1iSsv*5V0XXpm/&t+ t=ihYbP n6"vwzlwQ\wB"#,WwUQ"ABb{Hxr::tˆ1`!BF dV1z@Β||#2nPBĐ)$#BB5< D1e#.@*xO^Ocm'A4~$2^)oW4}Q5=Ype }'oG0Oxkǁ'gЍ#.TyP{,ٯ#.Nߜ#.ƅ~Ȧ#,.CP(^viq/Wf#,$d/14wW@3Lf_u*<{H"Q%EDM@1d671sn!X(b%EV"JFIh~Ċl&6|_/maJG=qb#2Tٔyo3$RF!EH,dlDj6,(0V-Efe*j;bJ^S5QQSz8"ֳ-OҢ7u͑MPhF6dbEI%*EDxEޣB[*k~h'0uĒwMuF/r_VʘbI~WQl!f!&`y #.pN3 S1x\FgߪX@Y"Or}%Wb{+&4Lq#D#.C9)#}c#SqJ!}BA}¡@S|ԖۤE))Pb"{*#2ݽ:EjN#,%#2xʔZYVD!,qqƨ&{1H;ˣW-f`&cжvfm?#2;M,X\.kui 1?rNJ4M.dfh[?P~"y؁#2iyJ+ut F#2#,mcdT*)W6ߕkIJY,إ^ &*2 %#yrK,aR411aGŠh6j@J_Av@wEK%#2?$B2*5`(j eFD`<5ҕYdC!h2&Z4ƒY$~h0>O&EXM]dlᙑ&Ņwݭ#._4D־hooݳIJ#2vZ+ƼVdےn3vfUuQW;PIl6.iVZMLl`m#2#2D8)V8mF-bk4"kˈղ[)21 ܩ%&0m0iK^E7b bDQFlx3h VMzKHC6h~)E>EFB #,@HSᒀƶn93E(~ 2PwL]#.r@lRHnްV]Z[֋bxar`+4C\Ty|\wb{Ccd@HR&2©p(`~6oܛQ˧i8Ӽ"eNa6Ad#2TԈ2'jq'-#.E,SS08!A(`7@;60D(T179![+v$VB2,/@JCL"T"h& >~^W︾Xc+Q&\J|aVڶolJ w: \m#.L#2R%;#.[RSV(b8lLzv9dT:n8Y ]#2qa,l-DEam_*j#.ROR7ENi+GqmtTPE4+YKcrxkhTJK0ԡmE F-⒨@19 Q8ssC2ӡa i47'# <rHH.C=dC {;= tW5lr46z\JAIb(2[ &x[B!B'А]xO'=S#,w=KlJ9  #,-#,Ϳ??~}~ϧ?/o믧?~??g?|ߚ9̟P@ԃuH`H#,!Pv9sL Ui@[My ~#2M ?TuHfg[mUwLa))mg~ #2#,f[Cq_BgGhoF/|_1 ;Gn @ "q!P,g-S }ZTx$uE":D]’ !\=BhұڲK#,#2iI(7| nqsBNɚn#2{.##2DäQ@`$ mr69#2(#,1Cr@ʡ#,X1$DFiIHqw*‰ ^K&?.fd`adEBL8B#3RQ9ni6֚D񸚉CPmk4[h)J)#2#"_AJ YP*oP xU7E[)lY:@a.ԤEހ߰F`S%&$+'J2!! !~]ɶ!Z.O4[V2E:rԱT>^EhRvpIP.6S kJMqYɴSݾ4JC͏CP&æ#.QH},Xj!>VUKU_Ŭmo}<g/(m\`lqn$ZQ|F7ޘo%Ԗ+jʷ+Wx|<ג(2YFۮt,\9!ȢE3&)B!xneN2ApqQb8nYbSnq!4w JXQAʒp =7툺ƒY"B;%z=܎\0GX5>QOYXEGqݢG[\ٶJ5[mRoνjFѨoJ@|=g>tB <>|ghk2YiY 0٣ )<~8D@M_~i4ygԁb"p:iT"pwo}ztb4Л843;O}O1d!ӌDPKH@(=0JJh\]DۧGg.v|8V~873vM)n'DS8&\e|!ttYX剼#p尖7'ו .φ`Zd]aQB(e׿P`vd%b}rW%m#2oQ9JAk"Oι^S]B@h鑀 +#<== +#-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCgAdFiEEjH6y+TsMRfVzL+XRG6xXHc13IpUFAmkVHSoACgkQG6xXHc13\nIpXDwhAAtbEeolKXInrphJBMw+HF4wnuUCO9NVLkfszN0YyCr3fUbM5T1/lpAuAq\nPuWLH7UEjTuaanWg7q4EJmGKZvpSPaOozIWD4hEYDhYNDPhEGLfn7HaTyFnFNPBD\nQyx9IrKQnu5RlIwx9OoU7zI4SgQyLcjHLSBBQt2GWz9ioueMoW9Ooy6l08ggct/S\n6mQCjZOqVRR6RlRnbP4u9hJTsS8/QpPs76k4USjC9wn1MOY4L+WrIslJVdFBYU+p\nQH5gAHvOEKCpp9PmMxBf6n5weqXAB8oilJUZrhcOjZ0wLkMfr2uJjhbvYH/76X0z\nVeJsfwGGU3JuXeV2qQQeACh66KVQu6WjYKydlhymz1G4WLsYrhNoCFCkoH/7rcLJ\nEpWwRVSPBIkW5hdeQfukBqJYNei7T+NFtZ28+7kfQMNvTnYozOjft9MRO1awf/Eo\ncryjZ88B5NrI/DRtrGeWJcWhwrVfphMPw5/facGV8NGkWKHRJCCdfKsmJO6aDiBQ\n4gN41UD7VYtZvRHQH2f1i+wL0qQSI7dCzXuZ53gZJ80DKPmzuYosFDVonVi7qTLX\nQIh1krF68c+BYU7cXX2H2vtdPuVjCH0Xw30UGbElKj3uMtx2SaK3R1IsrxySbPrv\n4luPoBGo0gon1sO5vi35v1Xl9Aa7ZbD21XsyCQ5mR4lZfp4t6Os=\n=mE93\n-----END PGP SIGNATURE-----\n diff --git a/wscript b/wscript new file mode 100644 index 0000000..3effc7d --- /dev/null +++ b/wscript @@ -0,0 +1,142 @@ +# ========================================================================= +# CException - Simple Exception Handling in C +# ThrowTheSwitch.org +# Copyright (c) 2007-24 Mark VanderVoord +# SPDX-License-Identifier: MIT +# ========================================================================= + +""" """ + +from waflib import Context, Errors, Logs, Utils +from waflib.Tools import waf_unit_test + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from waflib.Build import BuildContext + from waflib.Configure import ConfigurationContext + from waflib.Options import OptionsContext + + +def options(opt: OptionsContext): + """Provide basic C compiler and testing options""" + # we can select the C compiler we want to test with using the + # --check-c-compiler option + opt.load("compiler_c") + opt.load("waf_unit_test") + + +def configure(cnf: ConfigurationContext): + """Configure the C compiler and environment for the host platform""" + cnf.load("compiler_c") + cnf.load("waf_unit_test") + + # this setup is for running bare tests on CException + # and not the integration tests that require Ceedling/Unity + # and friends + cnf.env.append_unique("DEFINES", ["BARE_TEST"]) + + if cnf.env.CC_NAME == "gcc": + # when using GCC we can build using coverage + cnf.env.append_unique("CFLAGS", ["--coverage"]) + # we need the coverage library during linking + cnf.check_cc(lib="gcov", uselib_store="GCOV") + cnf.env.append_unique("LINKFLAGS", ["-coverage"]) + + # We need Python and gcovr to create a coverage report + # from the coverage data + cnf.find_program("python", var="PYTHON") + cnf.find_program("gcovr", mandatory=False) + if cnf.env.GCOVR: + cnf.env.gcovr_module = "gcovr" + + +def build(bld: BuildContext): + """Build the library and tests and then run the tests.""" + # build CException using the custom configuration file as this is + # how later want to test it, so we need to define + # CEXCEPTION_USE_CONFIG_FILE + # We also need to define TEST, as otherwise extended tests would run + # that are not meaningful when multithreading is NOT enabled + bld( + features="c", + source="lib/CException.c", + target="CEXCEPTION.o", + defines=["TEST", "CEXCEPTION_USE_CONFIG_FILE"], + includes="../test/support", + ) + # archive the CException object into a library + bld(features="c cstlib", use="CEXCEPTION.o", target="CEXCEPTION") + + # build, link and run the test binary + bld( + features="c cprogram test", + source="test/TestException.c", + target="test", + use="CEXCEPTION GCOV", + includes="lib test/support", + defines=["TEST", "CEXCEPTION_USE_CONFIG_FILE"], + ) + + # postprocess unit testing results + bld.add_post_fun(std) # print entire stdout/stderr + bld.add_post_fun(gcovr) # create the coverage report + bld.add_post_fun(waf_unit_test.summary) + bld.add_post_fun(waf_unit_test.set_exit_code) + + +def gcovr(bld: BuildContext): + """Create the coverage report.""" + # gcovr is only loaded for gcc and clang as reporting does not work for + # MSVC + if not bld.env.GCOVR: + Logs.warn("Cannot generate coverage report") + return + python = Utils.subst_vars("${PYTHON}", bld.env) + gcovr_module = Utils.subst_vars("${gcovr_module}", bld.env) + root = bld.srcnode.abspath() + cwd = bld.bldnode + cmd = [ + python, + "-m", + gcovr_module, + "-r", + root, + "--html-details", + "-o", + "index.html", + "lib", + ] + + try: + bld.cmd_and_log(cmd, cwd=cwd, quiet=True, output=Context.BOTH) + except Errors.WafError as e: + if e.stdout: + Logs.pprint("NORMAL", e.stdout) + if e.stderr: + Logs.error(e.stderr) + bld.fatal("gcovr error.") + report_file = bld.bldnode.find_node("index.html") + Logs.pprint("NORMAL", f"\nReport: {report_file}") + +def std(bld: BuildContext): + """Print stdout and stderr output to the terminal after running tests.""" + lst = getattr(bld, "utest_results", []) + if not lst: + return + + nr_tests = len(lst) + nr_failed_tests = len([x for x in lst if x[1]]) + + val = 100 * (nr_tests - nr_failed_tests) / (1.0 * nr_tests) + Logs.pprint("CYAN", f"test report {val:3.0f}% success") + + Logs.pprint("CYAN", f" tests that fail {nr_failed_tests}/{nr_tests}") + for result in lst: + Logs.pprint("CYAN", f" {result.test_path} ({result.generator.name})") + color = "RED" if result.exit_code else "GREEN" + Logs.pprint(color, f"status: {result.exit_code}") + if result.out: + Logs.pprint("NORMAL", f"out:\n{result.out.decode('utf-8')}") + if result.err: + Logs.pprint("RED", f"err:\n{result.err.decode('utf-8')}") From f7efb393de0115d5b580a9e746b711b153e58bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 11:46:38 +0200 Subject: [PATCH 02/20] Fix target typo --- wscript | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wscript b/wscript index 3effc7d..5f279b7 100644 --- a/wscript +++ b/wscript @@ -61,19 +61,19 @@ def build(bld: BuildContext): bld( features="c", source="lib/CException.c", - target="CEXCEPTION.o", + target="obj-cexception", defines=["TEST", "CEXCEPTION_USE_CONFIG_FILE"], includes="../test/support", ) # archive the CException object into a library - bld(features="c cstlib", use="CEXCEPTION.o", target="CEXCEPTION") + bld(features="c cstlib", use="obj-cexception", target="cexception") # build, link and run the test binary bld( features="c cprogram test", source="test/TestException.c", target="test", - use="CEXCEPTION GCOV", + use="cexception GCOV", includes="lib test/support", defines=["TEST", "CEXCEPTION_USE_CONFIG_FILE"], ) From c652f8f2e5f2f127372e0f91869e2c5dd598414a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 11:49:45 +0200 Subject: [PATCH 03/20] fix binary output name --- wscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wscript b/wscript index 5f279b7..1f8211f 100644 --- a/wscript +++ b/wscript @@ -72,7 +72,7 @@ def build(bld: BuildContext): bld( features="c cprogram test", source="test/TestException.c", - target="test", + target="TestException", use="cexception GCOV", includes="lib test/support", defines=["TEST", "CEXCEPTION_USE_CONFIG_FILE"], From 445d265dee3350279688ea936e72c42748ce760e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 11:58:51 +0200 Subject: [PATCH 04/20] Add Windws GCC build --- .github/workflows/main.yml | 57 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d3f0db9..458045a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ on: jobs: # Job: Unit test suite unit-tests: - # name: "Unit Tests" + # name: "Unit Tests (Ceedling GCC)" # runs-on: ubuntu-latest # steps: # # Checks out repository under $GITHUB_WORKSPACE @@ -60,4 +60,57 @@ jobs: - name: Run build and test run: | source .venv/bin/activate - python waf configure build -v \ No newline at end of file + python waf configure build -v + + unit-tests-windows-msys2-native: + name: "Unit Tests (MSYS2 GCC)" + runs-on: windows-latest + + steps: + - name: Checkout Latest Repo + uses: actions/checkout@v4 + + - name: Install MSYS2 + uses: msys2/setup-msys2@v2 + with: + update: true + install: >- + mingw-w64-x86_64-gcc + mingw-w64-x86_64-make + mingw-w64-x86_64-binutils + + - name: Add MSYS2 MinGW64 to PATH + shell: pwsh + run: | + echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Check GCC + shell: pwsh + run: gcc --version + + - name: Install Python 3 (native Windows) + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~\AppData\Local\pip\Cache + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Create virtual environment + shell: pwsh + run: | + python -m venv .venv + .\.venv\Scripts\activate + pip install --upgrade pip + pip install -r requirements.txt + + - name: Run waf build with MSYS2 GCC + shell: pwsh + run: | + .\.venv\Scripts\activate + python waf configure build -v --check-c-compiler gcc \ No newline at end of file From 586a35aa99b5d400a486994e982f9e09a8d3de2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:03:40 +0200 Subject: [PATCH 05/20] Add Windows MSVC build --- .github/workflows/main.yml | 44 ++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 458045a..ef467f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,7 @@ jobs: # run: | # ceedling clobber test:all - name: "Unit Tests (Bare GCC)" + name: "Unit Tests (Linux/GCC/Bare)" runs-on: ubuntu-latest steps: - name: Checkout Latest Repo @@ -62,8 +62,8 @@ jobs: source .venv/bin/activate python waf configure build -v - unit-tests-windows-msys2-native: - name: "Unit Tests (MSYS2 GCC)" + unit-tests-windows-gcc-bare: + name: "Unit Tests (Windows/GCC/Bare)" runs-on: windows-latest steps: @@ -113,4 +113,40 @@ jobs: shell: pwsh run: | .\.venv\Scripts\activate - python waf configure build -v --check-c-compiler gcc \ No newline at end of file + python waf configure build -v --check-c-compiler gcc + + + unit-tests-windows-msvc-bare: + name: "Unit Tests (Windows/MSVC/Bare)" + runs-on: windows-latest + + steps: + - name: Checkout Latest Repo + uses: actions/checkout@v4 + + - name: Install Python 3 + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~\AppData\Local\pip\Cache + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Create virtual environment + shell: bash + run: | + python3 -m venv .venv + source .venv/Scripts/activate + pip install --upgrade pip + pip install -r requirements.txt + + - name: Run build and test (MSVC) + shell: bash + run: | + source .venv/Scripts/activate + python waf configure build -v --check-c-compiler msvc \ No newline at end of file From 76412b965550e7213ad943031873f8cc578786b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:08:33 +0200 Subject: [PATCH 06/20] Cleanup CI config --- .github/workflows/main.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ef467f9..d97f4e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: # steps: # # Checks out repository under $GITHUB_WORKSPACE # - name: Checkout Latest Repo - # uses: actions/checkout@v4 + # uses: actions/checkout@v7 # with: # submodules: recursive @@ -35,15 +35,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Latest Repo - uses: actions/checkout@v7.0.1 + uses: actions/checkout@v7 - name: Install Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: Cache pip packages - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} @@ -68,7 +68,7 @@ jobs: steps: - name: Checkout Latest Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install MSYS2 uses: msys2/setup-msys2@v2 @@ -89,12 +89,12 @@ jobs: run: gcc --version - name: Install Python 3 (native Windows) - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: Cache pip packages - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~\AppData\Local\pip\Cache key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} @@ -122,15 +122,15 @@ jobs: steps: - name: Checkout Latest Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: Cache pip packages - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~\AppData\Local\pip\Cache key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} @@ -138,15 +138,13 @@ jobs: ${{ runner.os }}-pip- - name: Create virtual environment - shell: bash run: | python3 -m venv .venv - source .venv/Scripts/activate + .\.venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txt - name: Run build and test (MSVC) - shell: bash run: | - source .venv/Scripts/activate + .\.venv\Scripts\activate python waf configure build -v --check-c-compiler msvc \ No newline at end of file From 6464bd937a372e3c936bc21b15501463cb7e78d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:10:02 +0200 Subject: [PATCH 07/20] Bring back Ceedling test --- .github/workflows/main.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d97f4e5..bd11e43 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,25 +11,25 @@ on: jobs: # Job: Unit test suite - unit-tests: - # name: "Unit Tests (Ceedling GCC)" - # runs-on: ubuntu-latest - # steps: - # # Checks out repository under $GITHUB_WORKSPACE - # - name: Checkout Latest Repo - # uses: actions/checkout@v7 - # with: - # submodules: recursive + unit-tests-linux-gcc-ceedling: + name: "Unit Tests (Linux/GCC/Ceedling)" + runs-on: ubuntu-latest + steps: + # Checks out repository under $GITHUB_WORKSPACE + - name: Checkout Latest Repo + uses: actions/checkout@v7 + with: + submodules: recursive - # # Install Ceedling - # - name: Install Ceedling - # run: | - # sudo gem install ceedling - - # # Run Tests - # - name: Run All Self Tests - # run: | - # ceedling clobber test:all + # Install Ceedling + - name: Install Ceedling + run: | + sudo gem install ceedling + + # Run Tests + - name: Run All Self Tests + run: | + ceedling clobber test:all name: "Unit Tests (Linux/GCC/Bare)" runs-on: ubuntu-latest From 8e55de790ce50b97fae76d63c6ed4a3a6fb33c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:11:28 +0200 Subject: [PATCH 08/20] Fix CI setup --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd11e43..69c97c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ jobs: run: | ceedling clobber test:all + unit-tests-linux-gcc-bare: name: "Unit Tests (Linux/GCC/Bare)" runs-on: ubuntu-latest steps: From 9eefd53d0214501136401b9fbad0d31a1bcb2c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:15:28 +0200 Subject: [PATCH 09/20] Setup ruby --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69c97c3..3e5e473 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,6 +20,12 @@ jobs: uses: actions/checkout@v7 with: submodules: recursive + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.1" + bundler-cache: true # Install Ceedling - name: Install Ceedling From 04e025e29d657b9b758f768fcbbafb0858fbe160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:23:19 +0200 Subject: [PATCH 10/20] use correct ruby --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e5e473..3e7f540 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,11 +26,11 @@ jobs: with: ruby-version: "3.1" bundler-cache: true - + # Install Ceedling - name: Install Ceedling run: | - sudo gem install ceedling + gem install ceedling --no-document # Run Tests - name: Run All Self Tests From 6a9fb7151c9f6812bd881b7e08ffffc3823cae4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:38:06 +0200 Subject: [PATCH 11/20] Add Clang tests on Linux --- .github/workflows/main.yml | 21 ++++++++++++++++ README.md | 12 ++++++--- wscript | 50 ++++++++++++++++++++++++++------------ 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e7f540..d7108f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,6 +69,27 @@ jobs: source .venv/bin/activate python waf configure build -v + unit-tests-linux-clang-bare: + name: "Unit Tests (Linux/Clang/Bare)" + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install clang + llvm-cov + run: | + sudo apt-get update + sudo apt-get install -y clang llvm + + - name: Check clang + run: clang --version + + - name: Build with clang + run: | + export CC=clang + python waf configure build -v --check-c-compiler clang + unit-tests-windows-gcc-bare: name: "Unit Tests (Windows/GCC/Bare)" runs-on: windows-latest diff --git a/README.md b/README.md index 0a119fe..64d650f 100644 --- a/README.md +++ b/README.md @@ -229,8 +229,14 @@ particular places. It is a rare instance where you will need these, but they are Testing ======= -If you want to validate that `CException` works with your tools or that it works with your custom +If you want to validate that CException works with your tools or that it works with your custom configuration, you may want to run the included test suite. This is the test suite (along with real projects we've used it on) that we use to make sure that things actually work the way we claim. -The test suite makes use of `Ceedling`, which uses the Unity Test Framework. It will require a native C compiler. -The example makefile and `rakefile` both use gcc. + +There are two ways of testing CException, using the unit testing frame work `Ceedling` or bare metal. +- `Ceedling`: The test suite makes use of `Ceedling`, which uses the Unity Test Framework. It will require a native C compiler. + The example makefile and `rakefile` both use gcc. +- Bare metal: The test suite makes use of `waf`, which is used as build an test framework. + It will require a native C compiler (GCC/Clang on Linux or GCC/MSVC on Windows). + Usage: `python waf configure build` + A coverage report is created if GCC or Clang are used and GCOVR is available. diff --git a/wscript b/wscript index 1f8211f..f61799f 100644 --- a/wscript +++ b/wscript @@ -43,9 +43,18 @@ def configure(cnf: ConfigurationContext): cnf.check_cc(lib="gcov", uselib_store="GCOV") cnf.env.append_unique("LINKFLAGS", ["-coverage"]) + if cnf.env.CC_NAME == "clang": + # when using GCC we can build using coverage + cnf.env.append_unique( + "CFLAGS", ["-fprofile-instr-generate", "-fcoverage-mapping"] + ) + cnf.env.append_unique( + "LINKFLAGS", ["-fprofile-instr-generate", "-fcoverage-mapping"] + ) + if cnf.env.CC_NAME in ("clang", "gcc"): # We need Python and gcovr to create a coverage report # from the coverage data - cnf.find_program("python", var="PYTHON") + cnf.find_program("python", var="PYTHON", mandatory=False) cnf.find_program("gcovr", mandatory=False) if cnf.env.GCOVR: cnf.env.gcovr_module = "gcovr" @@ -54,7 +63,7 @@ def configure(cnf: ConfigurationContext): def build(bld: BuildContext): """Build the library and tests and then run the tests.""" # build CException using the custom configuration file as this is - # how later want to test it, so we need to define + # how later want to test it, so we need to define # CEXCEPTION_USE_CONFIG_FILE # We also need to define TEST, as otherwise extended tests would run # that are not meaningful when multithreading is NOT enabled @@ -89,25 +98,33 @@ def gcovr(bld: BuildContext): """Create the coverage report.""" # gcovr is only loaded for gcc and clang as reporting does not work for # MSVC - if not bld.env.GCOVR: - Logs.warn("Cannot generate coverage report") + if not bld.env.GCOVR or not bld.env.PYTHON: + Logs.warn("Cannot generate coverage report.") return + gcovr_options = [] + if bld.env.CC_NAME == "clang": + gcovr_options = ["--llvm", "--gcov-executable=llvm-cov"] python = Utils.subst_vars("${PYTHON}", bld.env) gcovr_module = Utils.subst_vars("${gcovr_module}", bld.env) root = bld.srcnode.abspath() cwd = bld.bldnode - cmd = [ - python, - "-m", - gcovr_module, - "-r", - root, - "--html-details", - "-o", - "index.html", - "lib", - ] - + cmd = ( + [ + python, + "-m", + gcovr_module, + ] + + gcovr_options + + [ + "-r", + root, + "--html-details", + "-o", + "index.html", + "lib", + ] + ) + try: bld.cmd_and_log(cmd, cwd=cwd, quiet=True, output=Context.BOTH) except Errors.WafError as e: @@ -119,6 +136,7 @@ def gcovr(bld: BuildContext): report_file = bld.bldnode.find_node("index.html") Logs.pprint("NORMAL", f"\nReport: {report_file}") + def std(bld: BuildContext): """Print stdout and stderr output to the terminal after running tests.""" lst = getattr(bld, "utest_results", []) From 789609d57ee6f7642df83501b9241da4c9a465c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:47:24 +0200 Subject: [PATCH 12/20] Fix clang setup on Linux --- .github/workflows/main.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7108f8..404431e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,6 +77,26 @@ jobs: - name: Checkout uses: actions/checkout@v7 + - name: Install Python 3 + uses: actions/setup-python@v7 + with: + python-version: "3.x" + + - name: Cache pip packages + uses: actions/cache@v6 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Create virtual environment + run: | + python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + - name: Install clang + llvm-cov run: | sudo apt-get update @@ -87,6 +107,7 @@ jobs: - name: Build with clang run: | + source .venv/bin/activate export CC=clang python waf configure build -v --check-c-compiler clang From b8bc09efb6775a06accd850a4f0e81647f03a81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 12:55:49 +0200 Subject: [PATCH 13/20] Fix gcovr setup for Linux/Clang Add more strict compiler options for GCC and Clang --- test/TestException.c | 3 ++- wscript | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/test/TestException.c b/test/TestException.c index a3386c5..2f0725e 100644 --- a/test/TestException.c +++ b/test/TestException.c @@ -22,7 +22,8 @@ #define TEST_FAIL_MESSAGE(x) fprintf(stderr, "%s", x) #endif // TEST_FAIL_MESSAGE -void TEST_ASSERT_EQUAL(unsigned int a, CEXCEPTION_T b) { +// Due to 'test/support/CExceptionConfig.h' CEXCEPTION_T is defined as int. +void TEST_ASSERT_EQUAL(int a, CEXCEPTION_T b) { assert(a == b); } diff --git a/wscript b/wscript index f61799f..f0c2417 100644 --- a/wscript +++ b/wscript @@ -52,6 +52,16 @@ def configure(cnf: ConfigurationContext): "LINKFLAGS", ["-fprofile-instr-generate", "-fcoverage-mapping"] ) if cnf.env.CC_NAME in ("clang", "gcc"): + cnf.env.append_unique( + "CFLAGS", + [ + "-Wall", + "-Wextra", + "-Werror", + "-Wunused-function", # we need this so we can ensure all test functions are called! + ], + ) + # We need Python and gcovr to create a coverage report # from the coverage data cnf.find_program("python", var="PYTHON", mandatory=False) @@ -103,7 +113,11 @@ def gcovr(bld: BuildContext): return gcovr_options = [] if bld.env.CC_NAME == "clang": - gcovr_options = ["--llvm", "--gcov-executable=llvm-cov"] + gcovr_options = [ + "--gcov-executable=llvm-cov", + "--llvm-cov-binary=llvm-cov", + "--llvm-profdata-executable=llvm-profdata", + ] python = Utils.subst_vars("${PYTHON}", bld.env) gcovr_module = Utils.subst_vars("${gcovr_module}", bld.env) root = bld.srcnode.abspath() From 89e40a233a5d271dae0d2efdfcd4d93508c78bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 13:05:53 +0200 Subject: [PATCH 14/20] Try to set the correct profile file programmatically --- wscript | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wscript b/wscript index f0c2417..37046e5 100644 --- a/wscript +++ b/wscript @@ -51,6 +51,8 @@ def configure(cnf: ConfigurationContext): cnf.env.append_unique( "LINKFLAGS", ["-fprofile-instr-generate", "-fcoverage-mapping"] ) + cnf.env.LLVM_PROFILE_FILE="coverage.profraw" + if cnf.env.CC_NAME in ("clang", "gcc"): cnf.env.append_unique( "CFLAGS", From 9081a3e21146ee49189c6fba513039455ef25bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 13:13:08 +0200 Subject: [PATCH 15/20] next try for Linux/Clang --- .github/workflows/main.yml | 1 + wscript | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 404431e..9f11d1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -108,6 +108,7 @@ jobs: - name: Build with clang run: | source .venv/bin/activate + export LLVM_PROFILE_FILE="coverage.profraw" export CC=clang python waf configure build -v --check-c-compiler clang diff --git a/wscript b/wscript index 37046e5..daec473 100644 --- a/wscript +++ b/wscript @@ -51,7 +51,6 @@ def configure(cnf: ConfigurationContext): cnf.env.append_unique( "LINKFLAGS", ["-fprofile-instr-generate", "-fcoverage-mapping"] ) - cnf.env.LLVM_PROFILE_FILE="coverage.profraw" if cnf.env.CC_NAME in ("clang", "gcc"): cnf.env.append_unique( From 65fe3572aba81b207431bcd4fcc442c361925560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 13:15:19 +0200 Subject: [PATCH 16/20] Simplify Linux/Clang as coverage does not work --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f11d1d..4dbef04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -108,8 +108,6 @@ jobs: - name: Build with clang run: | source .venv/bin/activate - export LLVM_PROFILE_FILE="coverage.profraw" - export CC=clang python waf configure build -v --check-c-compiler clang unit-tests-windows-gcc-bare: From 756ba73b9d8f44ca05b1546cf6c1603ac018d86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 13:18:18 +0200 Subject: [PATCH 17/20] Reduce diff --- .github/workflows/main.yml | 1 + .gitignore | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4dbef04..5a8f454 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,7 @@ name: CI # Triggers the workflow on push or pull request events but only for the master branch on: push: + branches: [ master ] pull_request: branches: [ master ] diff --git a/.gitignore b/.gitignore index 969ed56..b65265b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ build *.sublime-* -.ruff_cache/ waf*-*-*/ -.lock-waf_* -.vscode/ \ No newline at end of file +.lock-waf_* \ No newline at end of file From e237f3e73256baf574acfbb5eb6becb37a7e241e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Sun, 23 Aug 2026 13:20:01 +0200 Subject: [PATCH 18/20] add missing docs --- wscript | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wscript b/wscript index daec473..8bd9902 100644 --- a/wscript +++ b/wscript @@ -5,7 +5,12 @@ # SPDX-License-Identifier: MIT # ========================================================================= -""" """ +"""Test CException for different compilers and platforms. + +Supported variants are: +- Linux: GCC, Clang +- Windows:. GCC, MSVC +""" from waflib import Context, Errors, Logs, Utils from waflib.Tools import waf_unit_test From a2cf1391e4344795058461e80637e81d670eefbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Tue, 25 Aug 2026 19:34:04 +0200 Subject: [PATCH 19/20] run pipeline --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 13c0ae6..df65944 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,7 @@ name: CI # Triggers the workflow on push or pull request events but only for the master branch on: push: - branches: [ master ] pull_request: - branches: [ master ] jobs: # Job: Unit test suite From ac4a9a979c6d819db694a781cd010e81eeb34994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Waldh=C3=B6r?= Date: Tue, 25 Aug 2026 19:47:33 +0200 Subject: [PATCH 20/20] Add macro definition --- test/support/CExceptionConfig.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/support/CExceptionConfig.h b/test/support/CExceptionConfig.h index d3e881e..ed1232a 100644 --- a/test/support/CExceptionConfig.h +++ b/test/support/CExceptionConfig.h @@ -13,8 +13,17 @@ #endif // BARE_TEST #ifdef BARE_TEST +#include +#include #include -#define TEST_FAIL_MESSAGE(x) fprintf(stderr, "%s", x) +#define TEST_ASSERT_GREATER_THAN_INT_MESSAGE(x, y, msg) \ +{ \ + bool violation = x > y; \ + if (violation) { \ + fprintf(stderr, "%s", msg); \ + assert(0);\ + } \ +} #endif // BARE_TEST extern volatile int TestingTheFallback;