2013.com.canonical.certification.checkbox-0.4/manage.py0000775000175000017500000000045012320565735022640 0ustar zygazyga00000000000000#!/usr/bin/env python3 from plainbox.provider_manager import setup from plainbox.provider_manager import N_ setup( name='2013.com.canonical.certification:checkbox', version="0.4", description=N_("Checkbox provider"), gettext_domain='2013.com.canonical.certification.checkbox', ) 2013.com.canonical.certification.checkbox-0.4/jobs/0000775000175000017500000000000012320573773021772 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/jobs/benchmarks.txt.in0000664000175000017500000002326112320565736025261 0ustar zygazyga00000000000000plugin: local id: benchmarks/disk/hdparm-read requires: device.category == 'DISK' _description: Benchmark for each disk command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: benchmarks/disk/hdparm-read_`ls /sys$path/block` requires: device.path == "$path" user: root command: hdparm -t /dev/`ls /sys$path/block | sed 's|!|/|'` | sed -e :a -e '$!N;s/\n/ /;ta' | sed 's/.*= *//' description: This test runs hdparm timing of device reads as a benchmark for $path EOF plugin: local id: benchmarks/disk/hdparm-cache-read requires: device.category == 'DISK' _description: Benchmark for each disk command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: benchmarks/disk/hdparm-cache-read_`ls /sys$path/block` requires: device.path == "$path" user: root command: hdparm -T /dev/`ls /sys$path/block | sed 's|!|/|'` | sed -e :a -e '$!N;s/\n/ /;ta' | sed 's/.*= *//' description: This test runs hdparm timing of cache reads as a benchmark for $path EOF plugin: shell id: benchmarks/graphics/gtkperf depends: graphics/xorg-version requires: package.name == 'gtkperf' command: python3 -c 'import re,sys,subprocess; (s, o) = subprocess.getstatusoutput("gtkperf -a"); [sys.exit(1) for i in [s] if s]; m = re.search("Total time:\s+(.*)\n", o); [print(i.group(1)+" Seconds") for i in [m] if m]' estimated_duration: 30.000 _description: Run gtkperf to make sure that GTK based test cases work plugin: shell id: benchmarks/graphics/render-bench requires: package.name == 'render-bench' command: /usr/bin/time -f "%e Seconds" render_bench 2>&1 >/dev/null estimated_duration: 52.000 _description: Run Render-Bench XRender/Imlib2 benchmark plugin: shell id: benchmarks/graphics/qgears2-Xrender-gearsfancy requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="qgears2.render-backend=1; qgears2.test-mode=0" pts_run qgears2 estimated_duration: 180.000 _description: Run Qgears2 XRender Extension gearsfancy benchmark plugin: shell id: benchmarks/graphics/qgears2-Xrender-compo requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="qgears2.render-backend=1; qgears2.test-mode=2" pts_run qgears2 estimated_duration: 31.500 _description: Run Qgears2 XRender Extension image scaling benchmark plugin: shell id: benchmarks/graphics/qgears2-gl-gearsfancy requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="qgears2.render-backend=2; qgears2.test-mode=0" pts_run qgears2 estimated_duration: 52.000 _description: Run Qgears2 OpenGL gearsfancy benchmark plugin: shell id: benchmarks/graphics/qgears2-gl-compo requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="qgears2.render-backend=2; qgears2.test-mode=2" pts_run qgears2 estimated_duration: 23.000 _description: Run Qgears2 OpenGL image scaling benchmark plugin: shell id: benchmarks/graphics/glmark2-es2 requires: package.name == 'glmark2-es2' 'arm' in cpuinfo.type command: glmark2-es2 2>&1 | sed -e :a -e '$!N;s/\n/ /;ta' | sed -E 's/.*(Score:\s+[0-9]+).*/\1/' _description: Run GLmark2-ES2 benchmark plugin: shell id: benchmarks/graphics/glmark2 requires: package.name == 'glmark2' cpuinfo.platform in ("i386", "x86_64") command: glmark2 2>&1 | sed -e :a -e '$!N;s/\n/ /;ta' | sed -E 's/.*(Score:\s+[0-9]+).*/\1/' estimated_duration: 306.000 _description: Run GLmark2 benchmark plugin: shell id: benchmarks/graphics/globs requires: package.name == 'globs' cpuinfo.platform in ("i386", "x86_64") command: glob_test --min-fps=26 --ignore-problems estimated_duration: 53.500 _description: Run globs benchmark plugin: shell id: benchmarks/graphics/unigine-sanctuary requires: package.name == 'phoronix-test-suite' command: pts_run unigine-sanctuary _description: Run Unigine Santuary benchmark plugin: shell id: benchmarks/graphics/unigine-tropics requires: package.name == 'phoronix-test-suite' command: pts_run unigine-tropics _description: Run Unigine Tropics benchmark plugin: shell id: benchmarks/graphics/unigine-heaven requires: package.name == 'phoronix-test-suite' command: pts_run unigine-heaven _description: Run Unigine Heaven benchmark plugin: shell id: benchmarks/graphics/lightsmark requires: package.name == 'phoronix-test-suite' command: pts_run lightsmark _description: Run Lightsmark benchmark plugin: shell id: benchmarks/memory/cachebench-read requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="cachebench.test=0" pts_run cachebench _description: Run Cachebench Read benchmark plugin: shell id: benchmarks/memory/cachebench-write requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="cachebench.test=1" pts_run cachebench _description: Run Cachebench Write benchmark plugin: shell id: benchmarks/memory/cachebench-read-modify-write requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="cachebench.test=2" pts_run cachebench _description: Run Cachebench Read / Modify / Write benchmark plugin: shell id: benchmarks/memory/stream-copy requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="stream.run-type=0" pts_run stream _description: Run Stream Copy benchmark plugin: shell id: benchmarks/memory/stream-scale requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="stream.run-type=1" pts_run stream _description: Run Stream Scale benchmark plugin: shell id: benchmarks/memory/stream-add requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="stream.run-type=2" pts_run stream _description: Run Stream Add benchmark plugin: shell id: benchmarks/memory/stream-triad requires: package.name == 'phoronix-test-suite' command: PRESET_OPTIONS="stream.run-type=3" pts_run stream _description: Run Stream Triad benchmark plugin: shell id: benchmarks/network/network-loopback requires: package.name == 'phoronix-test-suite' command: pts_run network-loopback estimated_duration: 85.0 _description: Run Network Loopback benchmark plugin: shell id: benchmarks/network/wifi_time_to_reconnect requires: device.category == 'WIRELESS' command: wifi_time2reconnect _description: Check the time needed to reconnect to a WIFI access point plugin: shell id: benchmarks/processor/encode-mp3 requires: package.name == 'phoronix-test-suite' command: pts_run encode-mp3 _description: Run Encode MP3 benchmark plugin: shell id: benchmarks/processor/x264 requires: package.name == 'phoronix-test-suite' command: pts_run x264 _description: Run x264 H.264/AVC encoder benchmark plugin: shell id: benchmarks/processor/gnupg requires: package.name == 'phoronix-test-suite' command: pts_run gnupg _description: Run GnuPG benchmark plugin: shell id: benchmarks/processor/compress-pbzip2 requires: package.name == 'phoronix-test-suite' command: pts_run compress-pbzip2 _description: Run Compress PBZIP2 benchmark plugin: shell id: benchmarks/processor/compress-7zip requires: package.name == 'phoronix-test-suite' command: pts_run compress-7zip _description: Run Compress 7ZIP benchmark plugin: shell id: benchmarks/processor/n-queens requires: package.name == 'phoronix-test-suite' command: pts_run n-queens _description: Run N-Queens benchmark plugin: shell id: benchmarks/processor/himeno requires: package.name == 'phoronix-test-suite' command: pts_run himeno _description: Run Himeno benchmark plugin: shell id: benchmarks/system/cpu_on_idle requires: package.name == 'sysstat' command: iostat -x -m 1 10 | python3 -c 'import sys, re; lines="".join(sys.stdin.readlines()); l=[float(n) for n in (re.findall("idle\n.*?(\S+)\n", lines))]; print(sum(l)/len(l),"%")' _description: CPU utilization on an idle system. plugin: shell id: benchmarks/system/disk_on_idle requires: package.name == 'sysstat' command: iostat -x -m 1 10 | python3 -c 'import sys, re; lines="".join(sys.stdin.readlines()); l=[float(n) for n in (re.findall("util\n.*?(\S+)\n", lines))]; print(sum(l)/len(l),"%")' _description: Disk utilization on an idle system. plugin: shell id: benchmarks/graphics/gputest_furmark_fullscreen_1920x1080 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark fur --width 1920 --height 1080 -f estimated_duration: 75.000 _description: Run a stress test based on FurMark (OpenGL 2.1 or 3.2) Fullscreen 1920x1080 no antialiasing plugin: shell id: benchmarks/graphics/gputest_furmark_windowed_1024x640 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark fur estimated_duration: 75.000 _description: Run a stress test based on FurMark (OpenGL 2.1 or 3.2) Windowed 1024x640 no antialiasing plugin: shell id: benchmarks/graphics/gputest_gimark_fullscreen_1920x1080 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark gi --width 1920 --height 1080 -f estimated_duration: 75.00 _description: Run GiMark, a geometry instancing test (OpenGL 3.3) Fullscreen 1920x1080 no antialiasing plugin: shell id: benchmarks/graphics/gputest_gimark_windowed_1024x640 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark gi estimated_duration: 75.500 _description: Run GiMark, a geometry instancing test (OpenGL 3.3) Windowed 1024x640 no antialiasing plugin: shell id: benchmarks/graphics/gputest_tessmark_fullscreen_1920x1080 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark tess --width 1920 --height 1080 -f estimated_duration: 75.000 _description: Run a tessellation test based on TessMark (OpenGL 4.0) Fullscreen 1920x1080 no antialiasing plugin: shell id: benchmarks/graphics/gputest_tessmark_windowed_1024x640 requires: package.name == 'gputest' cpuinfo.platform == 'x86_64' command: gputest_benchmark tess _description: Run a tessellation test based on TessMark (OpenGL 4.0) Windowed 1024x640 no antialiasing 2013.com.canonical.certification.checkbox-0.4/jobs/monitor.txt.in0000664000175000017500000000754412320565736024641 0ustar zygazyga00000000000000plugin: manual id: monitor/vga requires: display.vga == 'supported' _description: PURPOSE: This test will check your VGA port. STEPS: Skip this test if your system does not have a VGA port. 1. Connect a display (if not already connected) to the VGA port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/dvi requires: display.dvi == 'supported' _description: PURPOSE: This test will check your DVI port. STEPS: Skip this test if your system does not have a DVI port. 1. Connect a display (if not already connected) to the DVI port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/displayport requires: display.dp == 'supported' _description: PURPOSE: This test will check your DisplayPort port. STEPS: Skip this test if your system does not have a DisplayPort port. 1. Connect a display (if not already connected) to the DisplayPort port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/hdmi requires: display.hdmi == 'supported' _description: PURPOSE: This test will check your HDMI port. STEPS: Skip this test if your system does not have a HDMI port. 1. Connect a display (if not already connected) to the HDMI port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/svideo requires: display.svideo == 'supported' _description: PURPOSE: This test will check your S-VIDEO port. STEPS: Skip this test if your system does not have a S-VIDEO port. 1. Connect a display (if not already connected) to the S-VIDEO port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/rca requires: display.rca == 'supported' _description: PURPOSE: This test will check your RCA port. STEPS: Skip this test if your system does not have a RCA port. 1. Connect a display (if not already connected) to the RCA port on your system VERIFICATION: Was the desktop displayed correctly on both screens? plugin: manual id: monitor/multi-head requires: dmi.product in ['Desktop','Low Profile Desktop','Tower','Mini Tower'] _description: PURPOSE: This test verifies that multi-monitor output works on your desktop system. This is NOT the same test as the external monitor tests you would run on your laptop. You will need two monitors to perform this test. STEPS: Skip this test if your video card does not support multiple monitors. 1. If your second monitor is not already connected, connect it now 2. Open the "Displays" tool (open the dash and search for "Displays") 3. Configure your output to provide one desktop across both monitors 4. Open any application and drag it from one monitor to the next. VERIFICATION: Was the stretched desktop displayed correctly across both screens? plugin: user-interact-verify id: monitor/powersaving command: xset dpms force off _description: PURPOSE: This test will check your monitor power saving capabilities STEPS: 1. Click "Test" to try the power saving capabilities of your monitor 2. Press any key or move the mouse to recover VERIFICATION: Did the monitor go blank and turn on again? plugin: user-interact-verify id: monitor/dim_brightness requires: dmi.product in ['Notebook','Laptop','Portable'] user: root command: brightness_test _description: PURPOSE: This test will test changes to screen brightness STEPS: 1. Click "Test" to try to dim the screen. 2. Check if the screen was dimmed approximately to half of the maximum brightness. 3. The screen will go back to the original brightness in 2 seconds. VERIFICATION: Was your screen dimmed approximately to half of the maximum brightness? 2013.com.canonical.certification.checkbox-0.4/jobs/smoke.txt.in0000664000175000017500000000213612320565736024260 0ustar zygazyga00000000000000plugin: shell id: smoke/true command: true _description: Check success result from shell test case plugin: shell id: smoke/false command: false _description: Check failed result from shell test case plugin: shell id: smoke/dependency/good depends: smoke/true command: true _description: Check job is executed when dependency succeeds plugin: shell id: smoke/dependency/bad depends: smoke/false command: true _description: Check job result is set to uninitiated when dependency fails plugin: shell id: smoke/requirement/good requires: package.name == "checkbox" command: true _description: Check job is executed when requirements are met plugin: shell id: smoke/requirement/bad requires: package.name == "unknown-package" command: true _description: Check job result is set to "not required on this system" when requirements are not met plugin: manual id: smoke/manual _description: PURPOSE: This test checks that the manual plugin works fine STEPS: 1. Add a comment 2. Set the result as passed VERIFICATION: Check that in the report the result is passed and the comment is displayed 2013.com.canonical.certification.checkbox-0.4/jobs/ethernet.txt.in0000664000175000017500000001077612320565736024771 0ustar zygazyga00000000000000plugin: shell id: ethernet/detect requires: device.category == 'NETWORK' or device.category == 'WIRELESS' package.name == 'module-init-tools' package.name == 'pciutils' command: network_device_info estimated_duration: 1.2 _description: Test to detect the available network controllers plugin: shell id: ethernet/info_automated requires: package.name == 'network-manager' device.category == 'NETWORK' command: udev_resource | filter_templates -w "category=NETWORK"| awk "/path: / { print \$2 }"| xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do network_info \$i; done" estimated_duration: 30.0 _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: user-interact-verify id: ethernet/wired_connection command: network_check estimated_duration: 1.2 _description: PURPOSE: This test will check your wired connection STEPS: 1. Click on the Network icon in the top panel 2. Select a network below the "Wired network" section 3. Click "Test" to verify that it's possible to establish a HTTP connection VERIFICATION: Did a notification show and was the connection correctly established? plugin: local id: ethernet/multi_nic requires: device.category == 'NETWORK' _description: Automated test to walk multiple network cards and test each one in sequence. command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: ethernet/multi_nic_$2 requires: package.name == 'ethtool' package.name == 'nmap' device.path == "$1" user: root environ: TEST_TARGET_FTP TEST_TARGET_IPERF TEST_USER TEST_PASS command: network test -i $2 -t iperf --fail-threshold 80 estimated_duration: 330.0 description: Testing for NIC $2 EOF plugin: local id: ethernet/ethtool_info requires: device.category == 'NETWORK' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: ethernet/ethertool_check_$2 requires: device.path == "$1" command: ethtool $2 estimated_duration: 330.0 _description: This test executes ethtool requests against all the ethernet devices found on the system. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: local id: ethernet/maximum_bandwidth requires: device.category == 'NETWORK' package.name == 'zenity' package.name == 'iperf' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: user-verify user: root id: ethernet/maximum_bandwidth_$2 requires: device.path == "$1" command: network test -i $2 -t iperf 2>&1 | cat - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'ethernet max bw $2' estimated_duration: 330.0 _description: PURPOSE: User verification of whether the observed transfer throughput is acceptable for the type and maximum speed of each ethernet interface. STEPS: 1. Read the network test summary and confirm that the throughput is acceptable. 2. If needed, click "Test" again to repeat the transfer test. VERIFICATION: Was the reported throughput acceptable for the type and maximum speed of this interface? EOF _description: This test executes a maximum throughput test against all the ethernet devices found on the system. plugin: local id: ethernet/stress_performance requires: device.category == 'NETWORK' _description: Automated test that tests performance of each wired network device under stress. command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: ethernet/stress_performance_$2 requires: device.path == "$1" user: root command: network test -i $2 -t stress estimated_duration: 330.0 _description: This test executes iperf to generate a load on the network device and then performs a ping test to watch for dropped packets and very large latency periods. EOF 2013.com.canonical.certification.checkbox-0.4/jobs/mir.txt.in0000664000175000017500000000155712320565736023737 0ustar zygazyga00000000000000plugin: local id: mir/integration requires: package.name == 'mir-test-tools' _description: MIR Integration tests command: cat << 'EOF' | run_templates -s "mir_integration_tests --gtest_list_tests | sed -n '/\.$/s/\.$//p'" estimated_duration: 0.5 plugin: shell id: mir/integration/$1 requires: package.name == 'mir-test-tools' command: mir_integration_tests --gtest_filter=$1* _description: Run $1 test from MIR Integration tests. EOF plugin: local id: mir/acceptance requires: package.name == 'mir-test-tools' _description: MIR Acceptance tests command: cat << 'EOF' | run_templates -s "mir_acceptance_tests --gtest_list_tests | sed -n '/\.$/s/\.$//p'" estimated_duration: 0.5 plugin: shell id: mir/acceptance/$1 requires: package.name == 'mir-test-tools' command: mir_acceptance_tests --gtest_filter=$1* _description: Run $1 test from MIR Acceptance tests. EOF 2013.com.canonical.certification.checkbox-0.4/jobs/wireless.txt.in0000664000175000017500000004161312320565736025002 0ustar zygazyga00000000000000plugin: shell id: wireless/wireless_scanning requires: package.name == 'network-manager' device.category == 'WIRELESS' command: rfkill unblock wlan wifi if rfkill list wlan wifi | grep -q 'Hard blocked: yes'; then echo "Hard block is applied to WiFi device. Please remove and retest." exit 1 fi wireless_networks=`nmcli -f SSID dev wifi list` if [ `echo "$wireless_networks" | wc -l` -gt 1 ]; then echo "Wireless networks discovered: " echo "$wireless_networks" exit 0 fi echo "No wireless networks discovered." exit 1 estimated_duration: 0.645 _description: Wireless scanning test. It scans and reports on discovered APs. plugin: shell id: wireless/info_automated requires: package.name == 'network-manager' device.category == 'WIRELESS' command: udev_resource | filter_templates -w "category=WIRELESS"| awk "/path: / { print \$2 }"| xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do network_info \$i; done" estimated_duration: 1.2 _description: This is an automated test to gather some info on the current state of your wireless devices. If no devices are found, the test will exit with an error. plugin: user-interact-verify id: wireless/wireless_connection command: network_check estimated_duration: 120.0 requires: device.category == 'WIRELESS' _description: PURPOSE: This test will check your wireless connection. STEPS: 1. Click on the Network icon in the panel. 2. Select a network below the 'Wireless networks' section. 3. Click "Test" to verify that it's possible to establish an HTTP connection. VERIFICATION: Did a notification show and was the connection correctly established? plugin: shell id: wireless/wireless_connection_wpa_bg requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: WPA_BG_SSID WPA_BG_PSK command: trap "nmcli con delete id $WPA_BG_SSID" EXIT; create_connection wifi $WPA_BG_SSID --security=wpa --key=$WPA_BG_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11b/g protocols. plugin: shell id: wireless/wireless_connection_open_bg requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: OPEN_BG_SSID command: trap "nmcli con delete id $OPEN_BG_SSID" EXIT; create_connection wifi $OPEN_BG_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11b/g protocols. plugin: shell id: wireless/wireless_connection_wpa_n requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.n == 'supported' user: root environ: WPA_N_SSID WPA_N_PSK command: trap "nmcli con delete id $WPA_N_SSID" EXIT; create_connection wifi $WPA_N_SSID --security=wpa --key=$WPA_N_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11n protocol. plugin: shell id: wireless/wireless_connection_open_n requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.n == 'supported' user: root environ: OPEN_N_SSID command: trap "nmcli con delete id $OPEN_N_SSID" EXIT; create_connection wifi $OPEN_N_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11n protocol. plugin: shell id: wireless/wireless_connection_wpa_ac requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.ac == 'supported' user: root environ: WPA_AC_SSID WPA_AC_PSK command: trap "nmcli con delete id $WPA_AC_SSID" EXIT; create_connection wifi $WPA_AC_SSID --security=wpa --key=$WPA_AC_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11ac protocol. plugin: shell id: wireless/wireless_connection_open_ac requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.ac == 'supported' user: root environ: OPEN_AC_SSID command: trap "nmcli con delete id $OPEN_AC_SSID" EXIT; create_connection wifi $OPEN_AC_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11ac protocol. plugin: user-interact-verify id: wireless/wireless_connection_wpa_bg_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11b/g protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the B and G wireless bands 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: wireless/wireless_connection_open_bg_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11b/g protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the B and G wireless bands 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: wireless/wireless_connection_wpa_n_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' IEEE_80211.n == 'supported' IEEE_80211.band_5GHz == 'supported' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11n protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the N wireless band 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: wireless/wireless_connection_open_n_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' IEEE_80211.n == 'supported' IEEE_80211.band_5GHz == 'supported' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11n protocol. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the N wireless band 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: wireless/wireless_connection_wpa_ac_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' IEEE_80211.ac == 'supported' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11ac protocol. STEPS: 1. Open your router's configuration tool 2. Change the settings to only accept connections with the 802.11ac protocol. 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: wireless/wireless_connection_open_ac_manual requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' IEEE_80211.ac == 'supported' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 120.0 _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11ac protocol. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections with the 802.11ac protocol. 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: shell id: wireless/monitor_wireless_connection requires: package.name == 'iperf' device.category == 'WIRELESS' user: root environ: WPA_BG_SSID WPA_BG_PSK SERVER_IPERF command: trap "nmcli con delete id $WPA_BG_SSID" EXIT; create_connection wifi $WPA_BG_SSID --security=wpa --key=$WPA_BG_PSK && iperf -c $SERVER_IPERF -t 300 -i 30 estimated_duration: 330.0 _description: Tests the performance of a systems wireless connection through the iperf tool. plugin: shell id: wireless/monitor_wireless_connection_udp requires: package.name == 'iperf' device.category == 'WIRELESS' user: root environ: WPA_BG_SSID WPA_BG_PSK SERVER_IPERF command: trap "nmcli con delete id $WPA_BG_SSID" EXIT; create_connection wifi $WPA_BG_SSID --security=wpa --key=$WPA_BG_PSK && iperf -c $SERVER_IPERF -t 300 -i 30 -u -b 100m -p 5050 estimated_duration: 330.0 _description: Tests the performance of a systems wireless connection through the iperf tool, using UDP packets. plugin: shell id: wireless/wireless_connection_open_a requires: device.category == 'WIRELESS' IEEE_80211.band_5GHz == 'supported' user: root environ: OPEN_A_SSID command: trap "nmcli con delete id $OPEN_A_SSID" EXIT; create_connection wifi $OPEN_A_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Test that the system's wireless hardware can connect to a router using the 802.11a protocol. This requires that you have a router pre-configured to only respond to requests on the 802.11a protocol. plugin: shell id: wireless/wireless_connection_open_b requires: device.category == 'WIRELESS' user: root environ: OPEN_B_SSID command: trap "nmcli con delete id $OPEN_B_SSID" EXIT; create_connection wifi $OPEN_B_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Test that the system's wireless hardware can connect to a router using the 802.11b protocol. This requires that you have a router pre-configured to only respond to requests on the 802.11b protocol. plugin: local id: wireless/stress_performance requires: device.category == 'NETWORK' _description: Automated test that tests performance of each wireless network device under stress. command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=WIRELESS" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: wireless/stress_performance_$2 requires: device.path == "$1" user: root command: network test -i $2 -t stress estimated_duration: 330.0 _description: This test executes iperf to generate a load on the network device and then performs a ping test to watch for dropped packets and very large latency periods. EOF plugin: shell id: wireless/wireless_connection_open_g requires: device.category == 'WIRELESS' user: root environ: OPEN_G_SSID command: trap "nmcli con delete id $OPEN_G_SSID" EXIT; create_connection wifi $OPEN_G_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` estimated_duration: 30.0 _description: Test that the system's wireless hardware can connect to a router using the 802.11g protocol. This requires that you have a router pre-configured to only respond to requests on the 802.11g protocol. plugin: shell id: wireless/wireless_extension requires: device.category == 'WIRELESS' command: wireless_ext estimated_duration: 1.2 _description: Test that the MAC80211 modules are loaded and wireless extensions are working. plugin: local id: wireless/iwconfig_info requires: device.category == 'WIRELESS' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=WIRELESS" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: wireless/iwconfig_check_$2 requires: device.path == "$1" command: iwconfig $2 estimated_duration: 1.2 _description: This test executes iwconfig requests against all the ethernet devices found on the system. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: user-interact-verify id: wireless/wireless_rfkill command: rfkill list | zenity --text-info --title rfkill-Info estimated_duration: 120.0 requires: device.category == 'WIRELESS' _description: PURPOSE: This test will check whether or not your driver responds to rfkill commands. STEPS: 1. Use the hardware switch on the side of your device to switch off wireless. 2. If you do not have a hardware switch disable wireless from the network manager icon in the panel 3. Click "Test" to verify that the hard or soft blocks are in place. VERIFICATION: Did the hard or soft blocks show on in the dialog? plugin: local id: wireless/maximum_bandwidth requires: device.category == 'WIRELESS' package.name == 'zenity' package.name == 'iperf' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=WIRELESS" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: user-verify user: root id: wireless/maximum_bandwidth_$2 requires: device.path == "$1" command: network test -i $2 -t iperf 2>&1 | cat - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'wireless max bw $2' estimated_duration: 120.0 _description: PURPOSE: User verification of whether the observed transfer throughput is acceptable for the type and maximum speed of each wireless interface. STEPS: 1. Read the network test summary and confirm that the throughput is acceptable. 2. If needed, click "Test" again to repeat the transfer test. VERIFICATION: Was the reported throughput acceptable for the type and maximum speed of this interface? EOF _description: This test executes a maximum throughput test against all the wireless devices found on the system. 2013.com.canonical.certification.checkbox-0.4/jobs/install.txt.in0000664000175000017500000000051112320565736024603 0ustar zygazyga00000000000000plugin: shell id: install/apt-get-gets-updates requires: package.name == 'apt' user: root command: apt-get -d -y --force-yes dist-upgrade _description: Tests to see that apt can access repositories and get updates (does not install updates). This is done to confirm that you could recover from an incomplete or broken update. 2013.com.canonical.certification.checkbox-0.4/jobs/firmware.txt.in0000664000175000017500000000160112320567463024751 0ustar zygazyga00000000000000plugin: local id: firmware/fwts requires: package.name == 'fwts' _description: Automated tests for firmware using Firmware Test Suite. command: cat << 'EOF' | run_templates -s 'fwts_test --list' estimated_duration: 1.2 plugin: shell id: firmware/fwts_$1 requires: package.name == 'fwts' user: root command: fwts_test -t $1 -l $PLAINBOX_SESSION_SHARE/fwts_$1.log _description: Run $1 test from Firmware Test Suite. EOF plugin: local id: firmware/fwts_logs requires: package.name == 'fwts' _description: Automated tests for firmware using Firmware Test Suite. command: cat << 'EOF' | run_templates -s 'fwts_test --list' estimated_duration: 1.2 plugin: attachment id: firmware/fwts_$1.log requires: package.name == 'fwts' user: root command: [[ -e ${PLAINBOX_SESSION_SHARE}/fwts_$1.log ]] && cat ${PLAINBOX_SESSION_SHARE}/fwts_$1.log _description: Attach log for FWTS $1 test. EOF 2013.com.canonical.certification.checkbox-0.4/jobs/audio.txt.in0000664000175000017500000004221012320567463024237 0ustar zygazyga00000000000000plugin: shell id: audio/list_devices estimated_duration: 1.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' command: cat /proc/asound/cards _description: Test to detect audio devices plugin: user-interact-verify id: audio/playback_auto estimated_duration: 5.0 depends: audio/list_devices requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 2 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that internal speakers work correctly STEPS: 1. Make sure that no external speakers or headphones are connected If testing a desktop, external speakers are allowed 2. Click the Test button to play a brief tone on your audio device VERIFICATION: Did you hear a tone? plugin: user-interact-verify id: audio/playback_hdmi estimated_duration: 30.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --verbose --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --verbose --device=hdmi --volume=50; gst_pipeline_test -t 2 --device hdmi 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --verbose --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: HDMI audio interface verification STEPS: 1. Plug an external HDMI device with sound (Use only one HDMI/DisplayPort interface at a time for this test) 2. Click the Test button VERIFICATION: Did you hear the sound from the HDMI device? plugin: user-interact-verify id: audio/playback_displayport estimated_duration: 30.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --verbose --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --verbose --device=hdmi --volume=50; gst_pipeline_test -t 2 --device hdmi 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --verbose --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: DisplayPort audio interface verification STEPS: 1. Plug an external DisplayPort device with sound (Use only one HDMI/DisplayPort interface at a time for this test) 2. Click the Test button VERIFICATION: Did you hear the sound from the DisplayPort device? plugin: user-interact-verify id: audio/playback_headphones estimated_duration: 20.0 depends: audio/list_devices requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 2 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that headphones connector works correctly STEPS: 1. Connect a pair of headphones to your audio device 2. Click the Test button to play a sound to your audio device VERIFICATION: Did you hear a sound through the headphones and did the sound play without any distortion, clicks or other strange noises from your headphones? plugin: user-interact-verify id: audio/alsa_record_playback_internal estimated_duration: 20.0 depends: audio/playback_auto requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'pulseaudio-utils' package.name == 'gstreamer1.0-plugins-good' or package.name == 'gstreamer0.10-plugins-good' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; alsa_record_playback; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that recording sound using the onboard microphone works correctly STEPS: 1. Disconnect any external microphones that you have plugged in 2. Click "Test", then speak into your internal microphone 3. After a few seconds, your speech will be played back to you. VERIFICATION: Did you hear your speech played back? plugin: user-interact-verify id: audio/alsa_record_playback_external estimated_duration: 20.0 depends: audio/playback_headphones requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'pulseaudio-utils' package.name == 'gstreamer1.0-plugins-good' or package.name == 'gstreamer0.10-plugins-good' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; alsa_record_playback; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that recording sound using an external microphone works correctly STEPS: 1. Connect a microphone to your microphone port 2. Click "Test", then speak into the external microphone 3. After a few seconds, your speech will be played back to you VERIFICATION: Did you hear your speech played back? plugin: user-interact-verify id: audio/alsa_record_playback_usb estimated_duration: 120.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'pulseaudio-utils' package.name == 'gstreamer1.0-plugins-good' or package.name == 'gstreamer0.10-plugins-good' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=usb --volume=50; alsa_record_playback; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that a USB audio device works correctly STEPS: 1. Connect a USB audio device to your system 2. Click "Test", then speak into the microphone 3. After a few seconds, your speech will be played back to you VERIFICATION: Did you hear your speech played back through the USB headphones? plugin: shell id: audio/alsa_record_playback_automated estimated_duration: 10.0 requires: package.name == 'python3-gi' package.name == 'gir1.2-gstreamer-1.0' package.name == 'libgstreamer1.0-0' package.name == 'gstreamer1.0-plugins-good' package.name == 'gstreamer1.0-pulseaudio' package.name == 'alsa-base' device.category == 'AUDIO' command: audio_test _description: Play back a sound on the default output and listen for it on the default input. plugin: shell id: audio/alsa_info_collect estimated_duration: 2.0 command: alsa_info --no-dialog --no-upload --output ${PLAINBOX_SESSION_SHARE}/alsa_info.log _description: Collect audio-related system information. This data can be used to simulate this computer's audio subsystem and perform more detailed tests under a controlled environment. plugin: attachment id: audio/alsa_info_attachment depends: audio/alsa_info_collect estimated_duration: 1.0 command: [ -e ${PLAINBOX_SESSION_SHARE}/alsa_info.log ] && cat ${PLAINBOX_SESSION_SHARE}/alsa_info.log _description: Attaches the audio hardware data collection log to the results. plugin: user-interact-verify id: audio/channels estimated_duration: 20.0 command: speaker-test -c 2 -l 1 -t wav _description: PURPOSE: Check that the various audio channels are working properly STEPS: 1. Click the Test button VERIFICATION: You should clearly hear a voice from the different audio channels plugin: shell id: audio/check_volume estimated_duration: 1.0 requires: package.name == 'pulseaudio-utils' device.category == 'AUDIO' command: volume_test --minvol 1 --maxvol 100 _description: This test will verify that the volume levels are at an acceptable level on your local system. The test will validate that the volume is greater than or equal to minvol and less than or equal to maxvol for all sources (inputs) and sinks (outputs) recognized by PulseAudio. It will also validate that the active source and sink are not muted. You should not manually adjust the volume or mute before running this test. plugin: manual id: audio/external-lineout estimated_duration: 30.0 _description: PURPOSE: Check that external line out connection works correctly STEPS: 1. Insert cable to speakers (with built-in amplifiers) on the line out port 2. Open system sound preferences, 'Output' tab, select 'Line-out' on the connector list. Click the Test button 3. On the system sound preferences, select 'Internal Audio' on the device list and click 'Test Speakers' to check left and right channel VERIFICATION: 1. Do you hear a sound in the speakers? The internal speakers should *not* be muted automatically 2. Do you hear the sound coming out on the corresponding channel? plugin: user-interact-verify id: audio/external-linein estimated_duration: 120.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'pulseaudio-utils' package.name == 'gstreamer1.0-plugins-good' or package.name == 'gstreamer0.10-plugins-good' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; alsa_record_playback; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: Check that external line in connection works correctly STEPS: 1. Use a cable to connect the line in port to an external line out source. 2. Open system sound preferences, 'Input' tab, select 'Line-in' on the connector list. Click the Test button 3. After a few seconds, your recording will be played back to you. VERIFICATION: Did you hear your recording? plugin: user-interact id: audio/speaker-headphone-plug-detection estimated_duration: 60.0 requires: device.category == 'AUDIO' package.name == 'pulseaudio-utils' command: pulse-active-port-change sinks _description: PURPOSE: Check that system detects speakers or headphones being plugged in STEPS: 1. Prepare a pair of headphones or speakers with a standard 3.5mm jack 2. Locate the speaker / headphone jack on the device under test 3. Run the test (you have 30 seconds from now on) 4. Plug headphones or speakers into the appropriate jack 5. Unplug the device for subsequent tests. VERIFICATION: Verification is automatic, no action is required. The test times out after 30 seconds (and fails in that case). plugin: user-interact id: audio/microphone-plug-detection estimated_duration: 60.0 requires: device.category == 'AUDIO' package.name == 'pulseaudio-utils' command: pulse-active-port-change sources _description: PURPOSE: Check that system detects a microphone being plugged in STEPS: 1. Prepare a microphone with a standard 3.5mm jack 2. Locate the microphone jack on the device under test. Keep in mind that it may be shared with the headphone jack. 3. Run the test (you have 30 seconds from now on) 4. Plug the microphone into the appropriate jack 5. Unplug the device for subsequent tests. VERIFICATION: Verification is automatic, no action is required. The test times out after 30 seconds (and fails in that case). plugin: user-interact-verify id: audio/balance_internal_speaker estimated_duration: 20.0 depends: audio/playback_auto requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 10 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: Check that balance control works correctly on internal speakers STEPS: 1. Check that moving the balance slider from left to right works smoothly 2. Click the Test button to play an audio tone for 10 seconds. 3. Move the balance slider from left to right and back. 4. Check that actual speaker audio balance follows your setting. VERIFICATION: Does the slider move smoothly, as well as being followed by the setting by the actual audio output? plugin: user-interact-verify id: audio/balance_headphones depends: audio/playback_headphones estimated_duration: 30.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 10 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: Check that balance control works correctly on external headphone STEPS: 1. Check that moving the balance slider from left to right works smoothly 2. Click the Test button to play an audio tone for 10 seconds. 3. Move the balance slider from left to right and back. 4. Check that actual headphone audio balance follows your setting. VERIFICATION: Does the slider move smoothly, as well as being followed by the setting by the actual audio output? plugin: shell id: audio/list_devices_after_suspend_30_cycles estimated_duration: 1.0 depends: power-management/suspend_30_cycles requires: device.category == 'AUDIO' package.name == 'alsa-base' command: cat /proc/asound/cards _description: Test to detect audio devices after suspending 30 times. plugin: user-interact-verify id: audio/playback_auto_after_suspend_30_cycles estimated_duration: 5.0 depends: audio/list_devices power-management/suspend_30_cycles requires: device.category == 'AUDIO' package.name == 'alsa-base' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'pulseaudio-utils' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 2 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that internal speakers work correctly after suspending 30 times. STEPS: 1. Make sure that no external speakers or headphones are connected If testing a desktop, external speakers are allowed 2. Click the Test button to play a brief tone on your audio device VERIFICATION: Did you hear a tone? plugin: shell id: audio/alsa_record_playback_automated_after_suspend_30_cycles estimated_duration: 10.0 depends: power-management/suspend_30_cycles requires: package.name == 'python3-gi' package.name == 'gir1.2-gstreamer-1.0' package.name == 'libgstreamer1.0-0' package.name == 'gstreamer1.0-plugins-good' package.name == 'gstreamer1.0-pulseaudio' package.name == 'alsa-base' device.category == 'AUDIO' command: audio_test _description: Play back a sound on the default output and listen for it on the default input, after suspending 30 times. plugin: shell id: audio/check_volume_after_suspend_30_cycles estimated_duration: 1.0 depends: power-management/suspend_30_cycles requires: package.name == 'pulseaudio-utils' device.category == 'AUDIO' command: volume_test --minvol 1 --maxvol 100 _description: This test will verify that the volume levels are at an acceptable level on your local system. The test will validate that the volume is greater than or equal to minvol and less than or equal to maxvol for all sources (inputs) and sinks (outputs) recognized by PulseAudio. It will also validate that the active source and sink are not muted. You should not manually adjust the volume or mute before running this test. plugin: shell id: audio/audio_after_suspend_30_cycles estimated_duration: 1.0 depends: power-management/suspend_30_cycles requires: device.category == 'AUDIO' package.name == 'alsa-base' _description: Record mixer settings after suspending 30 times. command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/audio_settings_after_suspend_30_cycles; diff $PLAINBOX_SESSION_SHARE/audio_settings_before_suspend $PLAINBOX_SESSION_SHARE/audio_settings_after_suspend_30_cycles 2013.com.canonical.certification.checkbox-0.4/jobs/panel_clock_test.txt.in0000664000175000017500000000207312320565736026453 0ustar zygazyga00000000000000id: panel_clock/verify plugin: manual requires: package.name == 'gnome-system-tools' _description: PURPOSE: This test will verify that the desktop clock displays the correct date and time STEPS: 1. Check the clock in the upper right corner of your desktop. VERIFICATION: Is the clock displaying the correct date and time for your timezone? id: panel_clock/test plugin: user-interact-verify depends: panel_clock/verify requires: package.name == 'gnome-system-tools' user: root command: date -s "`date -d '1 hour'`" _description: PURPOSE: This test will verify that the desktop clock synchronizes with the system clock. STEPS: 1. Click the "Test" button and verify the clock moves ahead by 1 hour. Note: It may take a minute or so for the clock to refresh 2. Right click on the clock, then click on "Time & Date Settings..." 3. Ensure that your clock application is set to manual. 4. Change the time 1 hour back 5. Close the window and reboot VERIFICATION: Is your system clock displaying the correct date and time for your timezone? 2013.com.canonical.certification.checkbox-0.4/jobs/firewire.txt.in0000664000175000017500000000245412320565736024761 0ustar zygazyga00000000000000plugin: user-interact id: firewire/insert command: removable_storage_watcher insert firewire _description: PURPOSE: This test will check the system can detect the insertion of a FireWire HDD STEPS: 1. Click 'Test' to begin the test. This test will timeout and fail if the insertion has not been detected within 20 seconds. 2. Plug a FireWire HDD into an available FireWire port. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result plugin: shell id: firewire/storage-test user: root depends: firewire/insert command: removable_storage_test -s 268400000 firewire _description: This is an automated test which performs read/write operations on an attached FireWire HDD plugin: user-interact id: firewire/remove depends: firewire/storage-test command: removable_storage_watcher remove firewire _description: PURPOSE: This test will check the system can detect the removal of a FireWire HDD STEPS: 1. Click 'Test' to begin the test. This test will timeout and fail if the removal has not been detected within 20 seconds. 2. Remove the previously attached FireWire HDD from the FireWire port. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result 2013.com.canonical.certification.checkbox-0.4/jobs/esata.txt.in0000664000175000017500000000244212320565736024237 0ustar zygazyga00000000000000plugin: user-interact id: esata/insert command: removable_storage_watcher insert ata_serial_esata _description: PURPOSE: This test will check the system can detect the insertion of an eSATA HDD STEPS: 1. Click 'Test' to begin the test. This test will timeout and fail if the insertion has not been detected within 20 seconds. 2. Plug an eSATA HDD into an available eSATA port. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result plugin: shell id: esata/storage-test user: root depends: esata/insert command: removable_storage_test -s 268400000 ata_serial_esata _description: This is an automated test which performs read/write operations on an attached eSATA HDD plugin: user-interact id: esata/remove depends: esata/storage-test command: removable_storage_watcher remove ata_serial_esata _description: PURPOSE: This test will check the system can detect the removal of an eSATA HDD STEPS: 1. Click 'Test' to begin the test. This test will timeout and fail if the removal has not been detected within 20 seconds. 2. Remove the previously attached eSATA HDD from the eSATA port. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result 2013.com.canonical.certification.checkbox-0.4/jobs/panel_reboot.txt.in0000664000175000017500000000073312320565736025614 0ustar zygazyga00000000000000plugin: manual id: panel_reboot_test _description: PURPOSE: This test will verify that you can reboot your system from the desktop menu STEPS: 1. Click the Gear icon in the upper right corner of the desktop and click on "Shut Down" 2. Click the "Restart" button on the left side of the Shut Down dialog 3. After logging back in, restart System Testing and it should resume here VERIFICATION: Did your system restart and bring up the GUI login cleanly? 2013.com.canonical.certification.checkbox-0.4/jobs/info.txt.in0000664000175000017500000001453412320565736024102 0ustar zygazyga00000000000000id: codecs_attachment plugin: attachment requires: device.driver == 'snd_hda_intel' command: cat /proc/asound/card*/codec#* estimated_duration: 0.023 _description: Attaches a report of installed codecs for Intel HDA id: cpuinfo_attachment plugin: attachment command: cat /proc/cpuinfo estimated_duration: 0.006 _description: Attaches a report of CPU information id: dmesg_attachment plugin: attachment command: cat /var/log/dmesg | ansi_parser estimated_duration: 0.640 _description: Attaches a copy of /var/log/dmesg to the test results id: dmi_attachment plugin: attachment command: [ -d /sys/class/dmi/id/ ] && (grep -r . /sys/class/dmi/id/ 2>/dev/null || true) || false estimated_duration: 0.044 _description: Attaches info on DMI id: dmidecode_attachment plugin: attachment requires: package.name == 'dmidecode' user: root command: dmidecode | iconv -t 'utf-8' -c estimated_duration: 0.030 _description: Attaches dmidecode output id: lshw_attachment plugin: attachment requires: package.name == 'lshw' user: root command: lshw | iconv -t 'utf-8' -c _description: Attaches lshw output id: efi_attachment plugin: attachment user: root command: [ -d /sys/firmware/efi ] && grep -m 1 -o --color=never 'EFI v.*' /var/log/kern.log* || true estimated_duration: 0.5 _description: Attaches the firmware version id: lspci_attachment plugin: attachment command: lspci -vvnn | iconv -t 'utf-8' -c estimated_duration: 0.042 _description: Attaches very verbose lspci output. id: lspci_network_attachment plugin: attachment command: lspci -vvnnQ | iconv -t 'utf-8' -c estimated_duration: 1.322 _description: Attaches very verbose lspci output (with central database Query). id: lsusb_attachment plugin: attachment requires: package.name == 'usbutils' user: root command: lsusb -vv | iconv -t 'utf-8' -c estimated_duration: 0.700 _description: List USB devices id: meminfo_attachment plugin: attachment command: cat /proc/meminfo estimated_duration: 0.043 id: modprobe_attachment plugin: attachment command: find /etc/modprobe.* -name \*.conf | xargs cat estimated_duration: 0.015 _description: Attaches the contents of the various modprobe conf files. id: modules_attachment plugin: attachment command: cat /etc/modules estimated_duration: 0.004 _description: Attaches the contents of the /etc/modules file. id: sysctl_attachment plugin: attachment command: find /etc/sysctl.* -name \*.conf | xargs cat estimated_duration: 0.014 _description: attaches the contents of various sysctl config files. id: sysfs_attachment plugin: attachment _description: Attaches a report of sysfs attributes. command: for i in `udevadm info --export-db | sed -n 's/^P: //p'`; do echo "P: $i" udevadm info --attribute-walk --path=/sys$i 2>/dev/null | sed -n 's/ ATTR{\(.*\)}=="\(.*\)"/A: \1=\2/p' echo done estimated_duration: 6.344 id: udev_attachment plugin: attachment command: udevadm info --export-db | xml_sanitize estimated_duration: 1.465 _description: Attaches a dump of the udev database showing system hardware information. id: udev_resource_attachment plugin: attachment command: udev_resource estimated_duration: 0.432 _description: Attaches the output of udev_resource, for debugging purposes id: gcov_attachment plugin: attachment requires: package.name == 'lcov' user: root command: gcov_tarball _description: Attaches a tarball of gcov data if present. id: lsmod_attachment plugin: attachment command: lsmod_info estimated_duration: 0.5 _description: Attaches a list of the currently running kernel modules. plugin: attachment id: acpi_sleep_attachment command: [ -e /proc/acpi/sleep ] && cat /proc/acpi/sleep estimated_duration: 0.5 _description: Attaches the contents of /proc/acpi/sleep if it exists. plugin: shell id: info/bootchart _description: Bootchart information. requires: package.name == 'bootchart' or package.name == 'pybootchartgui' user: root command: process_wait -u root bootchart collector ureadahead; \ [ `ls /var/log/bootchart/*.tgz 2>/dev/null | wc -l` -lt 2 ] && reboot && sleep 100 plugin: local id: info/hdparm _description: SATA/IDE device information. requires: package.name == 'hdparm' device.category == 'DISK' command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: attachment id: info/hdparm_`ls /sys$path/block`.txt requires: device.path == "$path" block_device.`ls /sys$path/block`_state != 'removable' user: root command: hdparm -I /dev/`ls /sys$path/block` EOF plugin: attachment id: bootchart.png depends: info/bootchart requires: package.name == 'pybootchartgui' _description: Attaches the bootchart png file for bootchart runs command: file=`ls /var/log/bootchart/*.png 2>/dev/null | tail -1`; \ [ -e "$file" ] && cat "$file" plugin: attachment id: bootchart.tgz depends: info/bootchart _description: Attaches the bootchart log for bootchart test runs. command: file=`ls /var/log/bootchart/*.tgz 2>/dev/null | tail -1`; \ [ -e "$file" ] && cat "$file" plugin: attachment id: installer_bootchart.tgz command: [ -e /var/log/installer/bootchart.tgz ] && cat /var/log/installer/bootchart.tgz _description: installs the installer bootchart tarball if it exists. plugin: attachment id: installer_debug.gz command: [ -e /var/log/installer/debug ] && gzip -9 -c /var/log/installer/debug estimated_duration: 0.1 _description: Attaches the installer debug log if it exists. plugin: attachment id: info/touchpad_driver requires: device.category == 'TOUCHPAD' command: touchpad_driver_info estimated_duration: 0.384 _description: Returns the name, driver name and driver version of any touchpad discovered on the system. plugin: attachment id: info/audio_device_driver requires: package.name == 'pulseaudio-utils' package.name == 'module-init-tools' device.category == 'AUDIO' command: audio_driver_info estimated_duration: 0.177 _description: Lists the device driver and version for all audio devices. plugin: attachment id: info/network_devices requires: device.category == 'NETWORK' or device.category == 'WIRELESS' package.name == 'module-init-tools' package.name == 'pciutils' command: network_device_info estimated_duration: 0.550 _description: Provides information about network devices plugin: attachment id: info/xrandr command: xrandr -q --verbose _description: Provides information about displays attached to the system plugin: attachment id: info/disk_partitions user: root command: parted -l _description: Attaches information about disk partitions 2013.com.canonical.certification.checkbox-0.4/jobs/peripheral.txt.in0000664000175000017500000000267312320565736025303 0ustar zygazyga00000000000000plugin: manual id: peripheral/printer _description: PURPOSE: This test will verify that a network printer is usable STEPS: 1. Make sure that a printer is available in your network 2. Click on the Gear icon in the upper right corner and then click on Printers 3. If the printer isn't already listed, click on Add 4. The printer should be detected and proper configuration values should be displayed 5. Print a test page VERIFICATION: Were you able to print a test page to the network printer? plugin: user-interact-verify id: peripheral/external-usb-modem command: network_check _description: PURPOSE: This test will verify that a USB DSL or Mobile Broadband modem works STEPS: 1. Connect the USB cable to the computer 2. Right click on the Network icon in the panel 3. Select 'Edit Connections' 4. Select the 'DSL' (for ADSL modem) or 'Mobile Broadband' (for 3G modem) tab 5. Click on 'Add' button 6. Configure the connection parameters properly 7. Notify OSD should confirm that the connection has been established 8. Select Test to verify that it's possible to establish an HTTP connection VERIFICATION: Was the connection correctly established? plugin: shell id: peripheral/external-usb-modem-http depends: peripheral/external-usb-modem command: wget -SO /dev/null http://$TRANSFER_SERVER _description: Automated test case to make sure that it's possible to download files through HTTP 2013.com.canonical.certification.checkbox-0.4/jobs/cpu.txt.in0000664000175000017500000000352212320567463023730 0ustar zygazyga00000000000000plugin: shell id: cpu/scaling_test requires: package.name == 'fwts' user: root environ: PLAINBOX_SESSION_SHARE command: fwts_test -t cpufreq -l ${PLAINBOX_SESSION_SHARE}/scaling_test.log _description: Test the CPU scaling capabilities using Firmware Test Suite (fwts cpufreq). plugin: attachment id: cpu/scaling_test-log-attach depends: cpu/scaling_test command: [[ -e ${PLAINBOX_SESSION_SHARE}/scaling_test.log ]] && cat ${PLAINBOX_SESSION_SHARE}/scaling_test.log _description: Attaches the log generated by cpu/scaling_test to the results plugin: shell id: cpu/maxfreq_test requires: package.name == 'fwts' user: root command: fwts_test -t maxfreq -l $PLAINBOX_SESSION_SHARE/maxfreq_test.log _description: Test that the CPU can run at its max frequency using Firmware Test Suite (fwts cpufreq). plugin: attachment id: cpu/maxfreq_test-log-attach depends: cpu/maxfreq_test command: [ -e $PLAINBOX_SESSION_SHARE/maxfreq_test.log ] && cat $PLAINBOX_SESSION_SHARE/maxfreq_test.log _description: Attaches the log generated by cpu/maxfreq_test to the results plugin: shell id: cpu/clocktest command: clocktest _description: Test for clock jitter. plugin: shell id: cpu/offlining_test user: root command: cpu_offlining _description: Test offlining CPUs in a multicore system. plugin: shell id: cpu/topology requires: int(cpuinfo.count) > 1 and (cpuinfo.platform == 'i386' or cpuinfo.platform == 'x86_64') command: cpu_topology _description: This test checks cpu topology for accuracy plugin: shell id: cpu/frequency_governors user: root command: nice -n -20 frequency_governors_test --debug _description: This test checks that CPU frequency governors are obeyed when set. plugin: shell id: cpu/arm_vfp_support requires: 'arm' in cpuinfo.type command: grep VFP /var/log/syslog _description: Validate that the Vector Floating Point Unit is running on ARM device 2013.com.canonical.certification.checkbox-0.4/jobs/sniff.txt.in0000664000175000017500000000245212320565736024250 0ustar zygazyga00000000000000plugin: user-interact id: sniff/sniff7 command: true _description: PURPOSE: To sniff things out STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test plugin: manual id: sniff/sniff6 _description: PURPOSE: To sniff things out STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test plugin: manual id: sniff/sniff5 _description: PURPOSE: To sniff things out STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test plugin: user-interact id: sniff/sniff4 command: reboot user: root _description: PURPOSE: Simulates a failure by rebooting the machine STEPS: 1. Click test to trigger a reboot 2. Select "Continue" once logged back in and checkbox is restarted VERIFICATION: You won't see the user-verify plugin: manual id: sniff/sniff3 _description: PURPOSE: If Recovery is successful, you will see this test on restarting checkbox, not sniff4. STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test plugin: manual id: sniff/sniff2 _description: PURPOSE: To sniff things out STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test plugin: manual id: sniff/sniff1 _description: PURPOSE: To sniff things out STEPS: 1. Click Yes VERIFICATION: None Necessary, this is a bogus test 2013.com.canonical.certification.checkbox-0.4/jobs/touchscreen.txt.in0000664000175000017500000000501412320565736025462 0ustar zygazyga00000000000000plugin: shell id: touchscreen/nontouch-automated requires: xinput.device_class == 'XITouchClass' and xinput.touch_mode != 'direct' command: true estimated_duration: 1.2 _description: Determine whether the screen is detected as a non-touch device automatically. plugin: shell id: touchscreen/multitouch-automated requires: xinput.device_class == 'XITouchClass' and xinput.touch_mode == 'direct' command: true estimated_duration: 1.2 _description: Determine whether the screen is detected as a multitouch device automatically. plugin: manual id: touchscreen/multitouch-manual depends: touchscreen/nontouch-automated estimated_duration: 120.0 _description: PURPOSE: Touchscreen manual detection of multitouch. STEPS: 1. Look at the specifications for your system. VERIFICATION: Is the screen supposed to be multitouch? plugin: manual id: touchscreen/tap-detect depends: touchscreen/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Check touchscreen tap recognition STEPS: 1. Tap an object on the screen with finger. The cursor should jump to location tapped and object should highlight VERIFICATION: Does tap recognition work? plugin: manual id: touchscreen/drag-n-drop depends: touchscreen/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Check touchscreen drag & drop STEPS: 1. Double tap, hold, and drag an object on the desktop 2. Drop the object in a different location VERIFICATION: Does the object select and drag and drop? plugin: manual id: touchscreen/multitouch-zoom depends: touchscreen/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Check touchscreen pinch gesture for zoom STEPS: 1. Place two fingers on the screen and pinch them together 2. Place two fingers on the screen and move then apart VERIFICATION: Does the screen zoom in and out? plugin: manual id: touchscreen/multitouch-window-move depends: touchscreen/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Validate that 3-touch drag is operating as expected STEPS: 1. Open a windows and bring it to the foreground 2. 3-touch the window and drag VERIFICATION: Did the window move along with the drag? plugin: manual id: touchscreen/multitouch-dash depends: touchscreen/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Validate that 4-touch tap is operating as expected STEPS: 1. 4-touch tap anywhere on the touchscreen VERIFICATION: Did the tap open the Dash? 2013.com.canonical.certification.checkbox-0.4/jobs/server-services.txt.in0000664000175000017500000000237312320565736026274 0ustar zygazyga00000000000000plugin: shell id: services/open_ssh_test requires: package.name == 'ssh' command: pgrep sshd >/dev/null || (echo 'FAIL: sshd is not running.' 2>&1 && false) _description: Verifies that sshd is running. plugin: shell id: services/print_server_test requires: package.name == 'cups' command: pgrep cupsd >/dev/null || (echo 'FAIL: cupsd is not running.' 2>&1 && false) _description: Verifies that Print/CUPs server is running. plugin: shell id: services/dns_server_test requires: package.name == 'bind9' package.name == 'dnsutils' user: root command: dns_server_test _description: Verifies that DNS server is running and working. plugin: shell id: services/samba_test requires: package.name == 'samba' package.name == 'winbind' user: root command: samba_test _description: Verifies that Samba server is running. plugin: shell id: services/lamp_test requires: package.name == 'apache2' package.name == 'php5-mysql' package.name == 'libapache2-mod-php5' package.name == 'mysql-server' user: root command: lamp_test _description: Verifies that the LAMP stack is running (Apache, MySQL and PHP). plugin: shell id: services/tomcat_test requires: package.name == 'tomcat6' user: root command: tomcat_test _description: Verifies that Tomcat server is running and working. 2013.com.canonical.certification.checkbox-0.4/jobs/floppy.txt.in0000664000175000017500000000063112320565736024451 0ustar zygazyga00000000000000plugin: local id: floppy/check requires: device.driver == 'floppy' _description: Floppy test command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "driver=floppy"' plugin: shell id: floppy/check_`ls /sys$path/driver/*/*/*/block` requires: device.path == "$path" description: Floppy test for $product user: root command: floppy_test /dev/`ls /sys$path/driver/*/*/*/block` EOF 2013.com.canonical.certification.checkbox-0.4/jobs/piglit.txt.in0000664000175000017500000000510012320567463024423 0ustar zygazyga00000000000000plugin: shell id: piglit/fbo requires: package.name == 'piglit' command: piglit_test -t ^spec/EXT_framebuffer_object -n fbo estimated_duration: 28.000 _description: Runs piglit tests for checking support for framebuffer object operations, depth buffer and stencil buffer plugin: shell id: piglit/gl-2.1 requires: package.name == 'piglit' command: piglit_test -t spec/'!OpenGL 2.1'/ -n gl-2.1 estimated_duration: 2.500 _description: Runs piglit tests for checking OpenGL 2.1 support plugin: shell id: piglit/vbo requires: package.name == 'piglit' command: piglit_test -t spec/ARB_vertex_buffer_object/ -n vbo estimated_duration: 0.430 _description: Runs piglit tests for checking support for vertex buffer object operations plugin: shell id: piglit/glsl-fragment-shader requires: package.name == 'piglit' command: piglit_test -t ^shaders/glsl-arb-fragment -n glsl-fragment-shader estimated_duration: 2.700 _description: Runs piglit tests for checking support for GLSL fragment shader operations plugin: shell id: piglit/glsl-vertex-shader requires: package.name == 'piglit' command: piglit_test -t ^shaders/glsl-clamp-vertex-color -t ^shaders/glsl-max-vertex-attrib -t ^shaders/glsl-novertexdata -n glsl-vertex-shader estimated_duration: 3.200 _description: Runs piglit tests for checking support for GLSL vertex shader operations plugin: shell id: piglit/glx-tfp requires: package.name == 'piglit' command: piglit_test -t glx-tfp -n glx-tfp estimated_duration: 2.600 _description: Runs piglit tests for checking support for texture from pixmap plugin: shell id: piglit/stencil_buffer requires: package.name == 'piglit' command: piglit_test -t glx-visuals-stencil -t readpixels-24_8 -n stencil_buffer estimated_duration: 30.000 _description: Runs piglit_tests for checking support for stencil buffer operations plugin: shell id: piglit/summarize_results requires: package.name == 'piglit' command: [ -e $PLAINBOX_SESSION_SHARE/piglit-results ] && piglit-summary-html.py $PLAINBOX_SESSION_SHARE/piglit-summary/ `find $PLAINBOX_SESSION_SHARE/piglit-results/ -name main` && echo "Successfully summarized piglit results. They are available in $PLAINBOX_SESSION_SHARE/piglit-sumary/" estimated_duration: 1.380 _description: Runs the piglit results summarizing tool plugin: shell id: piglit/tarball requires: package.name == 'piglit' depends: piglit/summarize_results command: [ -e $PLAINBOX_SESSION_SHARE/piglit-summary ] && tar cvfz $PLAINBOX_SESSION_SHARE/piglit-results.tar.gz $PLAINBOX_SESSION_SHARE/piglit-summary/ _description: Archives the piglit-summary directory into the piglit-results.tar.gz. 2013.com.canonical.certification.checkbox-0.4/jobs/bluetooth.txt.in0000664000175000017500000001052412320567463025146 0ustar zygazyga00000000000000 plugin: shell id: bluetooth/detect-output estimated_duration: 1.2 requires: package.name == 'bluez' device.category == 'BLUETOOTH' command: if rfkill list bluetooth | grep -q 'Hard blocked: yes'; then echo "rfkill shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes'; then echo "rfkill shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi output=$(hcitool dev | tail -n+2 | awk '{print $2}' | tee $PLAINBOX_SESSION_SHARE/bluetooth_address) echo "$output" if [ -z "$output" ]; then "BT hardware not available" exit 1 fi _description: Automated test to store bluetooth device information in checkbox report plugin: manual id: bluetooth/browse-files depends: bluetooth/detect-output estimated_duration: 120.0 _description: PURPOSE: This test will check that bluetooth connection works correctly STEPS: 1. Enable bluetooth on any mobile device (PDA, smartphone, etc.) 2. Click on the bluetooth icon in the menu bar 3. Select 'Setup new device' 4. Look for the device in the list and select it 5. In the device write the PIN code automatically chosen by the wizard 6. The device should pair with the computer 7. Right-click on the bluetooth icon and select browse files 8. Authorize the computer to browse the files in the device if needed 9. You should be able to browse the files VERIFICATION: Did all the steps work? plugin: manual id: bluetooth/file-transfer depends: bluetooth/browse-files bluetooth/detect-output estimated_duration: 120.0 _description: PURPOSE: This test will check that you can transfer information through a bluetooth connection STEPS: 1. Make sure that you're able to browse the files in your mobile device 2. Copy a file from the computer to the mobile device 3. Copy a file from the mobile device to the computer VERIFICATION: Were all files copied correctly? plugin: user-interact-verify id: bluetooth/audio-a2dp depends: bluetooth/detect-output estimated_duration: 120.0 command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; gst_pipeline_test -t 2 'audiotestsrc wave=sine freq=512 ! audioconvert ! audioresample ! autoaudiosink'; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will check that you can record and hear audio using a bluetooth audio device STEPS: 1. Enable the bluetooth headset 2. Click on the sound icon 3. Click "Sound Settings" 4. Look for the device in the list and select it 5. Set Quality to A2DP 6. Click "Test" to record for five seconds and reproduce in the bluetooth device VERIFICATION: Did you hear the sound? plugin: user-interact-verify id: bluetooth/audio depends: bluetooth/detect-output estimated_duration: 120.0 command: arecord -d 5 -D bluetooth -f S16_LE | aplay -D bluetooth -f S16_LE _description: PURPOSE: This test will check that you can record and hear audio using a bluetooth audio device STEPS: 1. Enable the bluetooth headset 2. Click on the bluetooth icon in the menu bar 3. Select 'Setup new device' 4. Look for the device in the list and select it 5. In the device write the PIN code automatically chosen by the wizard 6. The device should pair with the computer 7. Click the sound icon 8. Click "Sound Settings" 9. Select device and ensure Quality is set to "HSP/HFP" 10. Click "Test" to record for five seconds and reproduce in the bluetooth device VERIFICATION: Did you hear the sound you recorded in the bluetooth plugin: user-interact-verify id: bluetooth/HID depends: bluetooth/detect-output estimated_duration: 120.0 command: keyboard_test _description: PURPOSE: This test will check that you can use a BlueTooth HID device STEPS: 1. Enable either a BT mouse or keyboard 2. Click on the bluetooth icon in the menu bar 3. Select 'Setup new device' 4. Look for the device in the list and select it 5. For mice, perform actions such as moving the pointer, right and left button clicks and double clicks 6. For keyboards, click the Test button to lauch a small tool. Enter some text into the tool and close it. VERIFICATION: Did the device work as expected? 2013.com.canonical.certification.checkbox-0.4/jobs/fingerprint.txt.in0000664000175000017500000000243712320565736025475 0ustar zygazyga00000000000000plugin: manual id: fingerprint/login _description: PURPOSE: This test will verify that a fingerprint reader will work properly for logging into your system. This test case assumes that there's a testing account from which test cases are run and a personal account that the tester uses to verify the fingerprint reader STEPS: 1. Click on the User indicator on the left side of the panel (your user name). 2. Select "Switch User Account" 3. On the LightDM screen select your username. 4. Use the fingerprint reader to login. 5. Click on the user switcher applet. 6. Select the testing account to continue running tests. VERIFICATION: Did the authentication procedure work correctly? plugin: manual id: fingerprint/unlock _description: PURPOSE: This test will verify that a fingerprint reader can be used to unlock a locked system. STEPS: 1. Click on the Session indicator (Cog icon on the Left side of the panel) . 2. Select 'Lock screen'. 3. Press any key or move the mouse. 4. A window should appear that provides the ability to unlock either typing your password or using fingerprint authentication. 5. Use the fingerprint reader to unlock. 6. Your screen should be unlocked. VERIFICATION: Did the authentication procedure work correctly? 2013.com.canonical.certification.checkbox-0.4/jobs/stress.txt.in0000664000175000017500000002415512320567463024471 0ustar zygazyga00000000000000plugin: shell id: stress/cpu_stress_test requires: package.name == 'stress' user: root command: num_vm=$(awk '/MemTotal/ {x=$2/262144; print ((x == int(x)) ? x : int(x) +1)}' /proc/meminfo); vm_bytes=$(($(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)/$num_vm/4))M; stress --cpu `cpuinfo_resource | awk '/count:/ {print $2}'` --vm $num_vm --vm-bytes $vm_bytes --timeout 7200s _description: PURPOSE: Create jobs that use the CPU as much as possible for two hours. The test is considered passed if the system does not freeze. plugin: shell id: power-management/hibernate_30_cycles depends: power-management/hibernate_advanced requires: sleep.disk == 'supported' rtc.state == 'supported' environ: PLAINBOX_SESSION_SHARE user: root command: if type -P fwts >/dev/null; then echo "Calling fwts" fwts_test -l $PLAINBOX_SESSION_SHARE/hibernate_30_cycles -f none -s s4 --s4-device-check --s4-device-check-delay=45 --s4-sleep-delay=120 --s4-multiple=30 else echo "Calling sleep_test" set -o pipefail; sleep_test -s disk -i 30 -w 120 | tee $PLAINBOX_SESSION_SHARE/hibernate_30_cycles.log fi _description: PURPOSE: This is an automated stress test that will force the system to hibernate/resume for 30 cycles plugin: shell id: power-management/hibernate-30-cycles-log-check command: [ -e $PLAINBOX_SESSION_SHARE/hibernate_30_cycles.log ] && sleep_test_log_check -v s4 $PLAINBOX_SESSION_SHARE/hibernate_30_cycles.log _description: Automated check of the 30 cycle hibernate log for errors detected by fwts. plugin: attachment id: power-management/hibernate-30-cycle-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/hibernate_30_cycles.log ] && cat $PLAINBOX_SESSION_SHARE/hibernate_30_cycles.log _description: Attaches the log from the 30 cycle Hibernate/Resume test if it exists plugin: shell id: power-management/suspend_30_cycles depends: power-management/rtc suspend/suspend_advanced environ: PLAINBOX_SESSION_SHARE user: root command: if type -P fwts >/dev/null; then echo "Calling fwts" set -o pipefail; fwts_test -l $PLAINBOX_SESSION_SHARE/suspend_30_cycles -f none -s s3 --s3-device-check --s3-device-check-delay=45 --s3-sleep-delay=30 --s3-multiple=30 | tee $PLAINBOX_SESSION_SHARE/suspend_30_cycles_times.log else echo "Calling sleep_test" set -o pipefail; sleep_test -p -s mem -i 30 | tee $PLAINBOX_SESSION_SHARE/suspend_30_cycles.log fi _description: PURPOSE: This is an automated stress test that will force the system to suspend/resume for 30 cycles. plugin: shell id: power-management/suspend-30-cycles-log-check depends: power-management/suspend_30_cycles command: [ -e $PLAINBOX_SESSION_SHARE/suspend_30_cycles.log ] && sleep_test_log_check -v s3 $PLAINBOX_SESSION_SHARE/suspend_30_cycles.log _description: Automated check of the 30 cycle hibernate log for errors detected by fwts. plugin: attachment id: power-management/suspend-30-cycle-log-attach depends: power-management/suspend_30_cycles command: [ -e $PLAINBOX_SESSION_SHARE/suspend_30_cycles.log ] && cat $PLAINBOX_SESSION_SHARE/suspend_30_cycles.log _description: Attaches the log from the 30 cycle Suspend/Resume test if it exists plugin: shell id: power-management/suspend-30-cycles-time-check depends: power-management/suspend_30_cycles command: [ -e $PLAINBOX_SESSION_SHARE/suspend_30_cycles_times.log ] && sleep_time_check $PLAINBOX_SESSION_SHARE/suspend_30_cycles_times.log _description: Checks the sleep times to ensure that a machine suspends and resumes within a given threshold plugin: shell id: stress/hibernate_250_cycles depends: power-management/rtc environ: PLAINBOX_SESSION_SHARE user: root command: if type -P fwts >/dev/null; then echo "Calling fwts" fwts_test -l $PLAINBOX_SESSION_SHARE/hibernate_250_cycles -s s4 --s4-device-check --s4-device-check-delay=45 --s4-sleep-delay=120 --s4-multiple=250 else echo "Calling sleep_test" set -o pipefail; sleep_test -s disk -i 250 -w 120 | tee $PLAINBOX_SESSION_SHARE/hibernate_250_cycles.log fi _description: PURPOSE: This is an automated stress test that will force the system to hibernate/resume for 250 cycles plugin: attachment id: stress/hibernate-250-cycle-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/hibernate_250_cycles.log ] && cat $PLAINBOX_SESSION_SHARE/hibernate_250_cycles.log _description: Attaches the log from the 250 cycle Hibernate/Resume test if it exists plugin: shell id: stress/suspend_250_cycles depends: power-management/rtc environ: PLAINBOX_SESSION_SHARE user: root command: if type -P fwts >/dev/null; then echo "Calling fwts" set -o pipefail; fwts_test -l $PLAINBOX_SESSION_SHARE/suspend_250_cycles -s s3 --s3-device-check --s3-device-check-delay=45 --s3-sleep-delay=30 --s3-multiple=250 | tee $PLAINBOX_SESSION_SHARE/suspend_250_cycles_times.log else echo "Calling sleep_test" set -o pipefail; sleep_test -p -s mem -i 250 | tee $PLAINBOX_SESSION_SHARE/suspend_250_cycles.log fi _description: PURPOSE: This is an automated stress test that will force the system to suspend/resume for 250 cycles. plugin: attachment id: stress/suspend-250-cycle-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/suspend_250_cycles.log ] && cat $PLAINBOX_SESSION_SHARE/suspend_250_cycles.log _description: Attaches the log from the 250 cycle Suspend/Resume test if it exists plugin: shell id: stress/suspend-250-cycles-time-check command: [ -e $PLAINBOX_SESSION_SHARE/suspend_250_cycles_times.log ] && sleep_time_check $PLAINBOX_SESSION_SHARE/suspend_250_cycles_times.log _description: Checks the sleep times to ensure that a machine suspends and resumes within a given threshold plugin: shell id: stress/reboot requires: package.name == 'upstart' package.name == 'fwts' command: pm_test -r 100 --silent --log-level=notset reboot --log-dir=$PLAINBOX_SESSION_SHARE user: root environ: PLAINBOX_SESSION_SHARE _description: Stress reboot system (100 cycles) plugin: attachment id: stress/reboot_log depends: stress/reboot command: tar cvfz $PLAINBOX_SESSION_SHARE/stress_reboot.tgz $PLAINBOX_SESSION_SHARE/*reboot.100.log && cat $PLAINBOX_SESSION_SHARE/stress_reboot.tgz plugin: shell id: stress/poweroff requires: package.name == 'upstart' package.name == 'fwts' command: pm_test -r 100 --silent --log-level=notset poweroff --log-dir=$PLAINBOX_SESSION_SHARE user: root environ: PLAINBOX_SESSION_SHARE _description: Stress poweroff system (100 cycles) plugin: attachment id: stress/poweroff_log depends: stress/poweroff command: tar cvfz $PLAINBOX_SESSION_SHARE/stress_poweroff.tgz $PLAINBOX_SESSION_SHARE/*poweroff.100.log && cat $PLAINBOX_SESSION_SHARE/stress_poweroff.tgz plugin: shell id: stress/reboot_check depends: stress/reboot command: pm_log_check --log-level=notset $PLAINBOX_SESSION_SHARE/pm_test.reboot.100.log $PLAINBOX_SESSION_SHARE/pm_log_check_reboot.100.log _description: Check logs for the stress reboot (100 cycles) test case plugin: attachment id: stress/reboot_check_log depends: stress/reboot_check command: tar cvfz $PLAINBOX_SESSION_SHARE/stress_reboot_check.tgz $PLAINBOX_SESSION_SHARE/pm_log_check_reboot.100.log && cat $PLAINBOX_SESSION_SHARE/stress_reboot_check.tgz plugin: shell id: stress/poweroff_check depends: stress/poweroff command: pm_log_check --log-level=notset $PLAINBOX_SESSION_SHARE/pm_test.poweroff.100.log $PLAINBOX_SESSION_SHARE/pm_log_check_poweroff.100.log _description: Check logs for the stress poweroff (100 cycles) test case plugin: attachment id: stress/poweroff_check_log depends: stress/poweroff_check command: tar cvfz $PLAINBOX_SESSION_SHARE/stress_poweroff_check.tgz $PLAINBOX_SESSION_SHARE/pm_log_check_poweroff.100.log && cat $PLAINBOX_SESSION_SHARE/stress_poweroff_check.tgz plugin: shell id: stress/graphics requires: package.name == 'x11-apps' user: root environ: PLAINBOX_SESSION_SHARE command: graphics_stress_test -b repeat -d -o $PLAINBOX_SESSION_SHARE/graphics-stress-results && echo "Graphics Stress Test completed successfully" || echo "Graphics Stress Test completed, but there are errors. Please see the log $PLAINBOX_SESSION_SHARE/graphics-stress-results for details" && false _description: Run the graphics stress test. This test can take a few minutes. plugin: shell id: stress/graphics-tarball requires: package.name == 'x11-apps' depends: stress/graphics command: [ -e $PLAINBOX_SESSION_SHARE/graphics-stress-results ] && tar cvfz $PLAINBOX_SESSION_SHARE/graphics-stress-results.tar.gz $PLAINBOX_SESSION_SHARE/graphics-stress-results _description: Attaches the graphics stress results to the submission. plugin: shell id: stress/usb user: root command: removable_storage_test -s 10240000 -c 100 -i 3 usb _description: Runs a test that transfers 100 10MB files 3 times to usb. plugin: user-interact id: stress/sdhc user: root _summary: Stress test for SDHC card estimated_duration: 780.0 command: removable_storage_test -s 10240000 -c 100 -i 3 sdio scsi usb --memorycard _description: PURPOSE: This test will transfers 100 10MB files 3 times to a SDHC card, to check that the systems media card reader can transfer large amounts of data. STEPS: 1. Insert a SDHC card into the reader and then Click "Test". If a file browser opens up, you can safely close it. 2. Do not remove the device during this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: stress/network_restart user: root environ: PLAINBOX_SESSION_SHARE command: network_restart -t 1 -o $PLAINBOX_SESSION_SHARE _description: Ping ubuntu.com and restart network interfaces 100 times plugin: attachment id: stress/network_restart_log depends: stress/network_restart command: file=$PLAINBOX_SESSION_SHARE/network_restart.log; if [ -e "$file" ]; then iconv -t 'ascii' -c "$file"; fi plugin: manual id: stress/wireless_hotkey requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: To make sure that stressing the wifi hotkey does not cause applets to disappear from the panel or the system to lock up STEPS: 1. Log in to desktop 2. Press wifi hotkey at a rate of 1 press per second and slowly increase the speed of the tap, until you are tapping as fast as possible VERIFICATION: Verify the system is not frozen and the wifi and bluetooth applets are still visible and functional 2013.com.canonical.certification.checkbox-0.4/jobs/memory.txt.in0000664000175000017500000000052712320565736024454 0ustar zygazyga00000000000000plugin: shell id: memory/info user: root command: memory_compare _description: This test checks the amount of memory which is reporting in meminfo against the size of the memory modules detected by DMI. plugin: shell id: memory/check user: root requires: uname.name == 'Linux' command: memory_test _description: Test and exercise memory. 2013.com.canonical.certification.checkbox-0.4/jobs/keys.txt.in0000664000175000017500000001702412320567463024116 0ustar zygazyga00000000000000plugin: user-interact id: keys/lock-screen requires: device.category == 'KEYBOARD' command: lock_screen_watcher _description: PURPOSE: This test will test the screen lock key STEPS: 1. Press the Test button to begin this test. If there is no such key, please skip this test. 2. Press the lock screen button on the keyboard in 30 seconds. 3. If the screen is locked, move the mouse or press any key to activate the prompt. 4. Input the password to unlock the screen. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: manual id: keys/brightness requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will test the brightness key STEPS: 1. Press the brightness buttons on the keyboard VERIFICATION: Did the brightness change following to your key presses? plugin: user-interact-verify id: keys/volume requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe02e,0xe0ae:Volume Down' '0xe030,0xe0b0:Volume Up' _description: PURPOSE: This test will test the volume keys of your keyboard STEPS: Skip this test if your computer has no volume keys. 1. Click test to open a window on which to test the volume keys. 2. If all the keys work, the test will be marked as passed. VERIFICATION: Do the keys work as expected? plugin: user-interact-verify id: keys/mute requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe020,0xe0a0:Mute' _description: PURPOSE: This test will test the mute key of your keyboard STEPS: 1. Click test to open a window on which to test the mute key. 2. If the key works, the test will pass and the window will close. VERIFICATION: Does the mute key work as expected? plugin: manual id: keys/sleep requires: device.category == 'KEYBOARD' depends: suspend/suspend_advanced _description: PURPOSE: This test will test the sleep key STEPS: 1. Press the sleep key on the keyboard 2. Wake your system up by pressing the power button VERIFICATION: Did the system go to sleep after pressing the sleep key? plugin: user-interact-verify id: keys/battery-info requires: dmi.product in ['Notebook','Laptop','Portable'] user: root command: key_test -s '0xe071,0xef1:Battery Info' _description: PURPOSE: This test will test the battery information key STEPS: Skip this test if you do not have a Battery Button. 1. Click Test to begin 2. Press the Battery Info button (or combo like Fn+F3) 3: Close the Power Statistics tool if it opens VERIFICATION: Did the Battery Info key work as expected? plugin: manual id: keys/wireless requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will test the wireless key STEPS: 1. Press the wireless key on the keyboard 2. Check that the wifi LED turns off or changes color 3. Check that wireless is disabled 4. Press the same key again 5. Check that the wifi LED turns on or changes color 6. Check that wireless is enabled VERIFICATION: Did the wireless turn off on the first press and on again on the second? (NOTE: the LED functionality will be reviewed in a following test. Please only consider the functionality of the wifi itself here.) plugin: user-interact id: keys/media-control requires: device.category == 'KEYBOARD' user: root command: key_test -s 0xe010,0xe090:Previous 0xe024,0xe0a4:Stop 0xe019,0xe099:Next 0xe022,0xe0a2:Play _description: PURPOSE: This test will test the media keys of your keyboard STEPS: Skip this test if your computer has no media keys. 1. Click test to open a window on which to test the media keys. 2. If all the keys work, the test will be marked as passed. VERIFICATION: Do the keys work as expected? plugin: user-interact id: keys/super requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe05b,0xe0db:Left Super Key' _description: PURPOSE: This test will test the super key of your keyboard STEPS: 1. Click test to open a window on which to test the super key. 2. If the key works, the test will pass and the window will close. VERIFICATION: Does the super key work as expected? plugin: manual id: keys/video-out requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: Validate that the External Video hot key is working as expected STEPS: 1. Plug in an external monitor 2. Press the display hot key to change the monitors configuration VERIFICATION: Check that the video signal can be mirrored, extended, displayed on external or onboard only. plugin: manual id: keys/touchpad requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: Verify touchpad hotkey toggles touchpad functionality on and off STEPS: 1. Verify the touchpad is functional 2. Tap the touchpad toggle hotkey 3. Tap the touchpad toggle hotkey again VERIFICATION: Verify the touchpad has been disabled and re-enabled. plugin: manual id: keys/keyboard-backlight requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: Verify that the keyboard backlight toggle key works properly STEPS: 1. Tap the keyboard backlight key 2. Confirm that the keyboard backlight was toggled to the opposite state 3. Tap the keyboard backlight key again 4. Confirm that the keyboard backlight was toggled to the opposite state VERIFICATION: Did the keyboard backlight state change on each press? plugin: user-interact-verify id: keys/microphone-mute requires: device.category == 'AUDIO' device.category == 'KEYBOARD' package.name == 'alsa-base' package.name == 'pulseaudio-utils' package.name == 'gstreamer1.0-plugins-good' or package.name == 'gstreamer0.10-plugins-good' command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; audio_settings set --device=pci --volume=50; alsa_record_playback; EXIT_CODE=$?; audio_settings restore --file=$PLAINBOX_SESSION_SHARE/pulseaudio_settings; exit $EXIT_CODE _description: PURPOSE: This test will test the mute key for your microphone STEPS: 1. Click "Test" then speak: "Imagination is more important than knowledge" (or anything else) into your microphone. 2. While you are speaking, please press the mute key for the microphone to mute it and press it again to unmute. 3. After a few seconds, your speech will be played back to you. If the key works, your speech should be interrupted for a few seconds. VERIFICATION: Does the microphone mute key work as expected? plugin: manual id: keys/hibernate requires: dmi.product in ['Notebook','Laptop','Portable'] depends: power-management/hibernate_advanced _description: PURPOSE: This test will test the hibernate key STEPS: 1. Press the hibernate key on the keyboard 2. Check that the system hibernated correctly 3. Wake your system after hibernating by pressing the power button VERIFICATION: Did the system go to hibernate after pressing the hibernate key? plugin: manual id: keys/keyboard-overhead-light requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will test the keyboard overhead light key or switch STEPS: 1. Press the keyboard overhead light key or swtich on the light 2. Check the the keyboard overhead light turn on correctly 3. Press the key or switch again to toggle off the light VERIFICATION: Did the keyboard overhead light key or switch turns on and off the light? 2013.com.canonical.certification.checkbox-0.4/jobs/mediacard.txt.in0000664000175000017500000004351612320565736025062 0ustar zygazyga00000000000000plugin: user-interact id: mediacard/mmc-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Multimedia Card (MMC) media STEPS: 1. Click "Test" and then insert an MMC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/mmc-storage estimated_duration: 30.0 depends: mediacard/mmc-insert user: root command: removable_storage_test -s 67120000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/mmc-insert test is run. It tests reading and writing to the MMC card. plugin: user-interact id: mediacard/mmc-remove estimated_duration: 30.0 depends: mediacard/mmc-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of the MMC card from the systems card reader. STEPS: 1. Click "Test" and then remove the MMC card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/sd-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of an UNLOCKED Secure Digital (SD) media card STEPS: 1. Click "Test" and then insert an UNLOCKED SD card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/sd-storage estimated_duration: 30.0 depends: mediacard/sd-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sd-insert test is run. It tests reading and writing to the SD card. plugin: user-interact id: mediacard/sd-remove estimated_duration: 30.0 depends: mediacard/sd-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of an SD card from the systems card reader. STEPS: 1. Click "Test" and then remove the SD card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/sd-preinserted estimated_duration: 30.0 user: root requires: device.category == 'CARDREADER' command: removable_storage_test -s 268400000 --memorycard -l sdio usb scsi && removable_storage_test --memorycard sdio usb scsi _description: This is a fully automated version of mediacard/sd-automated and assumes that the system under test has a memory card device plugged in prior to checkbox execution. It is intended for SRU automated testing. plugin: user-interact id: mediacard/sdhc-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a UNLOCKED Secure Digital High-Capacity (SDHC) media card STEPS: 1. Click "Test" and then insert an UNLOCKED SDHC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/sdhc-storage estimated_duration: 30.0 depends: mediacard/sdhc-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sdhc-insert test is run. It tests reading and writing to the SDHC card. plugin: user-interact id: mediacard/sdhc-remove estimated_duration: 30.0 depends: mediacard/sdhc-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of an SDHC card from the systems card reader. STEPS: 1. Click "Test" and then remove the SDHC card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/cf-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Compact Flash (CF) media card STEPS: 1. Click "Test" and then insert a CF card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/cf-storage estimated_duration: 30.0 depends: mediacard/cf-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/cf-insert test is run. It tests reading and writing to the CF card. plugin: user-interact id: mediacard/cf-remove depends: mediacard/cf-storage estimated_duration: 30.0 command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a CF card from the systems card reader. STEPS: 1. Click "Test" and then remove the CF card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/sdxc-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Secure Digital Extended Capacity (SDXC) media card STEPS: 1. Click "Test" and then insert an UNLOCKED SDXC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/sdxc-storage estimated_duration: 30.0 depends: mediacard/sdxc-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sdxc-insert test is run. It tests reading and writing to the SDXC card. plugin: user-interact id: mediacard/sdxc-remove estimated_duration: 30.0 depends: mediacard/sdxc-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a SDXC card from the systems card reader. STEPS: 1. Click "Test" and then remove the SDXC card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/ms-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Memory Stick (MS) media card STEPS: 1. Click "Test" and then insert a MS card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/ms-storage estimated_duration: 30.0 depends: mediacard/ms-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/ms-insert test is run. It tests reading and writing to the MS card. plugin: user-interact id: mediacard/ms-remove estimated_duration: 30.0 depends: mediacard/ms-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a MS card from the systems card reader. STEPS: 1. Click "Test" and then remove the MS card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/msp-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Memory Stick Pro (MSP) media card STEPS: 1. Click "Test" and then insert a MSP card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/msp-storage estimated_duration: 30.0 depends: mediacard/msp-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/msp-insert test is run. It tests reading and writing to the MSP card. plugin: user-interact id: mediacard/msp-remove estimated_duration: 30.0 depends: mediacard/msp-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a MSP card from the systems card reader. STEPS: 1. Click "Test" and remove the MSP card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/xd-insert estimated_duration: 30.0 command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a Extreme Digital (xD) media card STEPS: 1. Click "Test" and then insert a xD card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/xd-storage estimated_duration: 30.0 depends: mediacard/xd-insert user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/xd-insert test is run. It tests reading and writing to the xD card. plugin: user-interact id: mediacard/xd-remove estimated_duration: 30.0 depends: mediacard/xd-insert command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a xD card from the systems card reader. STEPS: 1. Click "Test" and then remove the xD card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact-verify id: mediacard/sd-performance-manual depends: mediacard/sd-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'SD Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert SD card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/sdhc-performance-manual depends: mediacard/sdhc-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'SDHC Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert SDHC card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/mmc-performance-manual depends: mediacard/mmc-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'MMC Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert MMC card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/cf-performance-manual depends: mediacard/cf-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'CF Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert CF card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/sdxc-performance-manual depends: mediacard/sdxc-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'SDXC Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert SDXC card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/ms-performance-manual depends: mediacard/ms-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'MS Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert MS card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/msp-performance-manual depends: mediacard/msp-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'MSP Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert MSP card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? plugin: user-interact-verify id: mediacard/xd-performance-manual depends: mediacard/xd-insert estimated_duration: 120.0 user: root command: removable_storage_test -s 268400000 --memorycard sdio usb | cat <(echo "Working...") - <(echo; echo "Verify the result and click OK to decide on the outcome") | zenity --text-info --title 'XD Card Performance' _description: PURPOSE: This test will check your Media Card data transfer performance. STEPS: 1. Insert XD card into the reader slot on this computer. 2. Click the test button to perform the test. VERIFICATION: Did the results of the test match the expected performance of the inserted device? 2013.com.canonical.certification.checkbox-0.4/jobs/mobilebroadband.txt.in0000664000175000017500000000367212320565736026254 0ustar zygazyga00000000000000plugin: shell id: mobilebroadband/gsm_connection estimated_duration: 20.0 requires: package.name == 'network-manager' package.name == 'modemmanager' mobilebroadband.gsm == 'supported' user: root environ: GSM_CONN_NAME GSM_APN GSM_USERNAME GSM_PASSWORD command: trap "nmcli con delete id $GSM_CONN_NAME" EXIT; create_connection mobilebroadband gsm `if [ ${GSM_APN} ]; then echo "--apn=$GSM_APN"; fi` `if [ ${GSM_CONN_NAME} ]; then echo "--name=$GSM_CONN_NAME"; fi` `if [ ${GSM_USERNAME} ]; then echo "--username=$GSM_USERNAME"; fi` `if [ ${GSM_PASSWORD} ]; then echo "--password=$GSM_PASSWORD"; fi` && curl http://start.ubuntu.com/connectivity-check.html --interface `nmcli dev status | awk '/gsm/ {print $1}'`; if [ "`nmcli dev status | awk '/gsm/ {print $3}'`" == "connected" ]; then nmcli con down id `[ ${GSM_CONN_NAME} ] && echo "$GSM_CONN_NAME" || echo "MobileBB"`; fi _description: Creates a mobile broadband connection for a GSM based modem and checks the connection to ensure it's working. plugin: shell id: mobilebroadband/cdma_connection estimated_duration: 20.0 requires: package.name == 'network-manager' package.name == 'modemmanager' mobilebroadband.cdma == 'supported' user: root environ: CDMA_CONN_NAME CDMA_USERNAME CDMA_PASSWORD command: trap "nmcli con delete id $CDMA_CONN_NAME" EXIT; create_connection mobilebroadband cdma `if [ ${CDMA_CONN_NAME} ]; then echo "--name=$CDMA_CONN_NAME"; fi` `if [ ${CDMA_USERNAME} ]; then echo "--username=$CDMA_USERNAME"; fi` `if [ ${CDMA_PASSWORD} ]; then echo "--password=$CDMA_PASSWORD"; fi` && curl http://start.ubuntu.com/connectivity-check.html --interface `nmcli dev status | awk '/gsm/ {print $1}'`; if [ "`nmcli dev status | awk '/gsm/ {print $3}'`" == "connected" ]; then nmcli con down id `[ ${CDMA_CONN_NAME} ] && echo "$CDMA_CONN_NAME" || echo "MobileBB"`; fi _description: Creates a mobile broadband connection for a CDMA based modem and checks the connection to ensure it's working. 2013.com.canonical.certification.checkbox-0.4/jobs/graphics.txt.in0000664000175000017500000002300012320567463024732 0ustar zygazyga00000000000000plugin: shell id: graphics/driver_version command: graphics_driver estimated_duration: 0.500 _description: Parses Xorg.0.Log and discovers the running X driver and version plugin: shell id: graphics/xorg-version requires: package.name == "x11-utils" command: xdpyinfo | grep "^X.Org version" | cut -d ':' -f 2 | tr -d ' ' estimated_duration: 0.018 _description: Test to output the Xorg version plugin: manual id: graphics/resolution-change depends: graphics/xorg-version _description: PURPOSE: This test will verify that the GUI is usable after manually changing resolution STEPS: 1. Open the Displays application 2. Select a new resolution from the dropdown list 3. Click on Apply 4. Select the original resolution from the dropdown list 5. Click on Apply VERIFICATION: Did the resolution change as expected? plugin: shell id: graphics/xorg-process requires: package.name == 'xorg' package.name == 'procps' command: pgrep -f '/usr/bin/X' >/dev/null estimated_duration: 0.100 _description: Test that the X process is running. plugin: shell id: graphics/xorg-failsafe requires: package.name == 'xorg' command: ! test -e /var/log/Xorg.failsafe.log estimated_duration: 0.030 _description: Test that the X is not running in failsafe mode. plugin: user-verify id: graphics/resolution requires: device.category == 'VIDEO' command: resolution_test estimated_duration: 0.750 _description: PURPOSE: This test will verify the default display resolution STEPS: 1. This display is using the following resolution: INFO: $output VERIFICATION: Is this acceptable for your display? plugin: user-verify id: graphics/screen-resolution requires: device.category == 'VIDEO' package.name == 'qmlscene' command: timeout 5 qmlscene --transparent --fullscreen $PLAINBOX_PROVIDER_DATA/resolution_test.qml estimated_duration: 10 _description: PURPOSE: This test will verify the default display resolution STEPS: 1. Click on Test to display the screen resolution overlay for 5 seconds. VERIFICATION: Is this acceptable for your display? plugin: shell id: graphics/minimum_resolution requires: device.category == 'VIDEO' command: resolution_test --horizontal 800 --vertical 600 estimated_duration: 0.331 _description: Ensure the current resolution meets or exceeds the recommended minimum resolution (800x600). See here for details: . https://help.ubuntu.com/community/Installation/SystemRequirements plugin: user-verify id: graphics/maximum_resolution requires: device.category == 'VIDEO' package.name == 'zenity' command: zenity --info --text "Maximum resolution: $(xrandr -q |grep -A 1 "connected\( primary\)* [0-9]" |tail -1 |awk '{print $1}')" estimated_duration: 10 _description: PURPOSE: This test will verify the display is operating at its maximum supported resolution STEPS: 1. Consult the system's specifications and locate the screen's maximum supported resolution. 2. Click on Test to display the maximum resolution that can be used by Ubuntu on the current display. VERIFICATION: Is this the display's maximum resolution? id: graphics/modes plugin: shell command: graphics_modes_info estimated_duration: 0.250 _description: Collect info on graphics modes (screen resolution and refresh rate) id: graphics/color_depth plugin: shell command: color_depth_info estimated_duration: 0.150 _description: Collect info on color depth and pixel format. id: graphics/fresh_rate plugin: shell command: fresh_rate_info _description: Collect info on fresh rate. id: graphics/graphic_memory plugin: shell command: graphic_memory_info _description: Collect info on graphic memory. plugin: user-verify id: graphics/display requires: package.name == 'xorg' package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' command: gst_pipeline_test -t 2 'videotestsrc ! videoconvert ! autovideosink' || gst_pipeline_test -t 2 'videotestsrc ! ffmpegcolorspace ! autovideosink' _description: PURPOSE: This test will test the default display STEPS: 1. Click "Test" to display a video test. VERIFICATION: Do you see color bars and static? plugin: shell id: graphics/VESA_drivers_not_in_use command: cat /var/log/Xorg.0.log | perl -e '$a=0;while(<>){$a++ if /Loading.*vesa_drv\.so/;$a-- if /Unloading.*vesa/&&$a}exit 1 if $a' estimated_duration: 0.011 _description: Check that VESA drivers are not in use plugin: user-verify id: graphics/cycle_resolution requires: package.name == 'xorg' depends: graphics/VESA_drivers_not_in_use command: xrandr_cycle --screenshot-dir $PLAINBOX_SESSION_SHARE estimated_duration: 250.000 _description: PURPOSE: This test cycles through the detected video modes STEPS: 1. Click "Test" to start cycling through the video modes VERIFICATION: Did the screen appear to be working for each mode? plugin: user-verify id: graphics/rotation depends: graphics/xorg-version command: rotation_test estimated_duration: 20.000 _description: PURPOSE: This test will test display rotation STEPS: 1. Click "Test" to test display rotation. The display will be rotated every 4 seconds. 2. Check if all rotations (normal right inverted left) took place without permanent screen corruption VERIFICATION: Did the display rotation take place without without permanent screen corruption? plugin: shell id: graphics/compiz_check requires: package.name == 'nux-tools' command: ! /usr/lib/nux/unity_support_test -c -p | ansi_parser | grep ":\(\s\+\)no$" estimated_duration: 0.130 _description: Check that hardware is able to run compiz plugin: shell id: graphics/unity-support requires: package.name == 'nux-tools' command: ! /usr/lib/nux/unity_support_test -p | ansi_parser | grep ":\(\s\+\)no" estimated_duration: 0.131 _description: Check that hardware is able to run Unity 3D plugin: user-verify id: graphics/glxgears requires: package.name == 'mesa-utils' command: glxgears; true _description: PURPOSE: This test tests the basic 3D capabilities of your video card STEPS: 1. Click "Test" to execute an OpenGL demo. Press ESC at any time to close. 2. Verify that the animation is not jerky or slow. VERIFICATION: 1. Did the 3d animation appear? 2. Was the animation free from slowness/jerkiness? plugin: shell id: graphics/3d_window_open_close requires: package.name == 'mesa-utils' command: window_test -t open-close -i 10 estimated_duration: 60.525 _description: Open and close a 3D window multiple times plugin: shell id: graphics/3d_window_suspend_resume requires: package.name == 'mesa-utils' command: window_test -t suspend-resume -i 10 estimated_duration: 121.00 _description: Open, suspend resume and close a 3D window multiple times plugin: shell id: graphics/multi_3d_windows_open_close requires: package.name == 'mesa-utils' command: window_test -t open-close-multi -i 10 -w 4 estimated_duration: 60.000 _description: Open and close 4 3D windows multiple times plugin: shell id: graphics/3d_window_move requires: package.name == 'mesa-utils' command: window_test -t move estimated_duration: 50.000 _description: Move a 3D window around the screen plugin: shell id: graphics/screenshot requires: package.name == 'fswebcam' command: set -o pipefail; camera_test still --device=/dev/external_webcam -f ${PLAINBOX_SESSION_SHARE}/screenshot.jpg -q 2>&1 | ansi_parser _description: PURPOSE: Take a screengrab of the current screen (logged on Unity desktop) STEPS: 1. Take picture using USB webcam VERIFICATION: Review attachment manually later plugin: attachment id: screenshot.jpg depends: graphics/screenshot command: base64 ${PLAINBOX_SESSION_SHARE}/screenshot.jpg _description: Attaches the screenshot captured in graphics/screenshot. plugin: shell id: graphics/screenshot_fullscreen_video requires: package.name == 'fswebcam' command: [ -f ${PLAINBOX_PROVIDER_DATA}/video/Ogg_Theora_Video.ogv ] || { echo "Video file not found"; exit 1; } dbus-launch gsettings set org.gnome.totem repeat true totem --fullscreen ${PLAINBOX_PROVIDER_DATA}/video/Ogg_Theora_Video.ogv 2>/dev/null & set -o pipefail sleep 15 && camera_test still --device=/dev/external_webcam -f ${PLAINBOX_SESSION_SHARE}/screenshot_fullscreen_video.jpg -q 2>&1 | ansi_parser sleep 5 && totem --quit 2>/dev/null dbus-launch gsettings set org.gnome.totem repeat false _description: PURPOSE: Take a screengrab of the current screen during fullscreen video playback STEPS: 1. Start a fullscreen video playback 2. Take picture using USB webcam after a few seconds VERIFICATION: Review attachment manually later plugin: attachment id: screenshot_fullscreen_video.jpg depends: graphics/screenshot_fullscreen_video command: base64 ${PLAINBOX_SESSION_SHARE}/screenshot_fullscreen_video.jpg _description: Attaches the screenshot captured in graphics/screenshot_fullscreen_video. plugin: shell id: graphics/screenshot_opencv_validation requires: package.name == 'python-opencv' environ: EXTERNAL_WEBCAM_DEVICE command: screenshot_validation \ ${PLAINBOX_PROVIDER_DATA}/images/logo_Ubuntu_stacked_black.png \ --device=${EXTERNAL_WEBCAM_DEVICE:-/dev/external_webcam} \ -o ${PLAINBOX_SESSION_SHARE}/screenshot_opencv_validation.jpg _description: Take a screengrab of the screen displaying a black and white Ubuntu logo. Check that the screenshot matches the original file using OpenCV ORB detection. plugin: attachment id: screenshot_opencv_validation.jpg depends: graphics/screenshot_opencv_validation command: base64 ${PLAINBOX_SESSION_SHARE}/screenshot_opencv_validation.jpg _description: Attaches the screenshot captured in graphics/screenshot_opencv_validation. 2013.com.canonical.certification.checkbox-0.4/jobs/expresscard.txt.in0000664000175000017500000000051012320565736025457 0ustar zygazyga00000000000000plugin: manual id: expresscard/verification _description: PURPOSE: This will verify that an ExpressCard slot can detect inserted devices. STEPS: Skip this test if you do not have an ExpressCard slot. 1. Plug an ExpressCard device into the ExpressCard slot VERIFICATION: Was the device correctly detected? 2013.com.canonical.certification.checkbox-0.4/jobs/disk.txt.in0000664000175000017500000000663312320565736024102 0ustar zygazyga00000000000000plugin: shell id: disk/detect command: udev_resource | filter_templates -w "category=DISK" | awk -F': ' '$1 == "product" { print $2 }' _description: Detects and displays disks attached to the system. plugin: local id: disk/stats requires: device.category == 'DISK' _description: Check stats changes for each disk command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: disk/stats_`ls /sys$path/block` requires: device.path == "$path" block_device.`ls /sys$path/block`_state != 'removable' user: root command: disk_stats_test `ls /sys$path/block | sed 's|!|/|'` description: This test checks disk stats, generates some activity and rechecks stats to verify they've changed. It also verifies that disks appear in the various files they're supposed to. EOF plugin: local id: disk/smart requires: package.name == 'smartmontools' device.category == 'DISK' _description: SMART test command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: disk/smart_`ls /sys$path/block` requires: device.path == "$path" block_device.`ls /sys$path/block`_state != 'removable' description: This tests the SMART capabilities for $product (Note that this test will not work against hardware RAID) user: root command: disk_smart -b /dev/`ls /sys$path/block | sed 's|!|/|'` -s 130 -t 270 EOF plugin: local id: disk/read_performance requires: device.category == 'DISK' _description: Verify system storage performs at or above baseline performance command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: disk/read_performance_`ls /sys$path/block` requires: device.path == "$path" block_device.`ls /sys$path/block`_state != 'removable' description: Disk performance test for $product user: root command: disk_read_performance_test `ls /sys$path/block | sed 's|!|/|'` EOF plugin: local id: disk/storage_devices requires: device.category == 'DISK' _description: Verify that storage devices, such as Fibre Channel and RAID can be detected and perform under stress. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=DISK"' plugin: shell id: disk/storage_device_`ls /sys$path/block` user: root requires: device.path == "$path" block_device.`ls /sys$path/block`_state != 'removable' description: Disk I/O stress test for $product command: storage_test `ls /sys$path/block | sed 's|!|/|'` EOF plugin: shell id: disk/spindown requires: device.category == 'DISK' package.name == 'smartmontools' user: root command: spindown _description: Some new hard drives include a feature that parks the drive heads after a short period of inactivity. This is a power-saving feature, but it can have a bad interaction with the operating system that results in the drive constantly parked then activated. This produces excess wear on the drive, potentially leading to early failures. plugin: user-interact id: disk/hdd-parking requires: device.category == 'DISK' depends: input/accelerometer user: root command: hdd_parking _description: PURPOSE: This test checks that a systems drive protection mechanism works properly. STEPS: 1. Click on Test 2. Move the system under test around, ensuring it is raised and lowered at some point. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. 2013.com.canonical.certification.checkbox-0.4/jobs/usb.txt.in0000664000175000017500000001547312320565736023743 0ustar zygazyga00000000000000plugin: shell id: usb/detect estimated_duration: 1.0 command: lsusb | sed 's/.*\(ID .*\)/\1/' | head -n 4 || echo "No USB devices were detected" >&2 _description: Detects and shows USB devices attached to this system. plugin: user-interact-verify id: usb/disk_detect depends: usb/detect estimated_duration: 1.0 command: removable_storage_test -l usb _description: PURPOSE: This test will check that your system detects USB storage devices. STEPS: 1. Plug in one or more USB keys or hard drives. 2. Click on "Test". INFO: $output VERIFICATION: Were the drives detected? plugin: user-interact-verify id: usb/HID depends: usb/detect estimated_duration: 1.0 command: keyboard_test _description: PURPOSE: This test will check that you can use a USB HID device STEPS: 1. Enable either a USB mouse or keyboard 2. For mice, perform actions such as moving the pointer, right and left button clicks and double clicks 3. For keyboards, click the Test button to lauch a small tool. Type some text and close the tool. VERIFICATION: Did the device work as expected? plugin: user-interact id: usb/insert depends: usb/detect estimated_duration: 10.0 command: removable_storage_watcher insert usb _description: PURPOSE: This test will check that the system correctly detects the insertion of a USB storage device STEPS: 1. Click "Test" and insert a USB storage device, preferably a HDD. Although a USB pen drive may be used it might cause performance related tests to fail. (Note: this test will time-out after 20 seconds.) 2. Do not unplug the device after the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: usb3/insert requires: usb.usb3 == 'supported' estimated_duration: 10.0 command: removable_storage_watcher -m 500000000 insert usb _description: PURPOSE: This test will check that the system correctly detects the insertion of a USB 3.0 storage device STEPS: 1. Click "Test" and insert a USB 3.0 storage device, preferably a HDD, in a USB 3.0 port. Although a USB 3.0 pen drive may be used it might cause performance related tests to fail. (Note: this test will time-out after 20 seconds.) 2. Do not unplug the device after the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: usb/remove depends: usb/insert estimated_duration: 10.0 command: removable_storage_watcher remove usb _description: PURPOSE: This test will check that the system correctly detects the removal of a USB storage device STEPS: 1. Click "Test" and remove the USB device. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: usb3/remove depends: usb3/insert requires: usb.usb3 == 'supported' estimated_duration: 10.0 command: removable_storage_watcher -m 500000000 remove usb _description: PURPOSE: This test will check that the system correctly detects the removal of a USB 3.0 storage device STEPS: 1. Click "Test" and remove the USB 3.0 device. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact-verify id: usb/storage-transfer depends: usb/insert user: root estimated_duration: 45.0 command: removable_storage_test -s 268400000 usb _description: PURPOSE: This test will check your USB connection. STEPS: 1. Plug a USB HDD or thumbdrive into the computer. 2. An icon should appear on the Launcher. 3. Click "Test" to begin the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact-verify id: usb3/storage-transfer requires: usb.usb3 == 'supported' depends: usb3/insert user: root estimated_duration: 45.0 command: removable_storage_test -s 268400000 -m 500000000 usb _description: PURPOSE: This test will check your USB 3.0 connection. STEPS: 1. Plug a USB 3.0 HDD or thumbdrive into a USB 3.0 port in the computer. 2. An icon should appear on the Launcher. 3. Click "Test" to begin the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: usb/storage-automated depends: usb/insert user: root estimated_duration: 45.0 command: removable_storage_test -s 268400000 usb _description: This test is automated and executes after the usb/insert test is run. plugin: shell id: usb3/storage-automated requires: usb.usb3 == 'supported' depends: usb3/insert user: root estimated_duration: 45.0 command: removable_storage_test -s 268400000 -m 500000000 -p 7 usb _description: This test is automated and executes after the usb3/insert test is run. plugin: shell id: usb/storage-preinserted user: root estimated_duration: 45.0 command: removable_storage_test -l usb && removable_storage_test -s 268400000 usb _description: This is an automated version of usb/storage-automated and assumes that the server has usb storage devices plugged in prior to checkbox execution. It is intended for servers and SRU automated testing. plugin: shell id: usb3/storage-preinserted user: root requires: usb.usb3 == 'supported' estimated_duration: 45.0 command: removable_storage_test -l usb && removable_storage_test -s 268400000 -m 500000000 -p 7 usb _description: This is an automated version of usb3/storage-automated and assumes that the server has usb 3.0 storage devices plugged in prior to checkbox execution. It is intended for servers and SRU automated testing. plugin: manual id: usb/panels _description: PURPOSE: This test will check your USB connection. STEPS: 1. Connect a USB storage device to an external USB slot on this computer. 2. An icon should appear on the Launcher. 3. Confirm that the icon appears. 4. Eject the device. 5. Repeat with each external USB slot. VERIFICATION: Do all USB slots work with the device? plugin: shell id: usb/performance depends: usb/insert user: root estimated_duration: 45.00 command: removable_storage_test -s 268400000 -p 15 usb _description: This test will check that your USB 2.0 port transfers data at a minimum expected speed. plugin: shell id: usb3/superspeed_performance requires: usb.usb3 == 'supported' depends: usb3/insert user: root estimated_duration: 45.00 command: removable_storage_test -s 268400000 -m 500000000 -p 60 usb _description: This test will check that your USB 3.0 port transfers data at a minimum expected speed in accordance with the specification of USB 3.0 SuperSpeed mode. 2013.com.canonical.certification.checkbox-0.4/jobs/led.txt.in0000664000175000017500000001333112320565736023705 0ustar zygazyga00000000000000plugin: manual id: led/power _description: PURPOSE: Power LED verification STEPS: 1. Power LED should be on while device is switched on VERIFICATION: Does the power LED light as expected? plugin: manual id: led/power-blink-suspend depends: suspend/suspend_advanced _description: PURPOSE: Power LED verification STEPS: 1. The Power LED should blink or change color while the system is suspended VERIFICATION: Did the Power LED blink or change color while the system was suspended for the previous suspend test? plugin: manual id: led/suspend _description: PURPOSE: Suspend LED verification. STEPS: Skip this test if your system does not have a dedicated Suspend LED. 1. The Suspend LED should blink or change color while the system is suspended VERIFICATION Did the Suspend LED blink or change color while the system was suspended? plugin: manual id: led/battery-charging _description: PURPOSE: Validate that the battery light shows charging status STEPS: 1. Let system run on battery for a while 2. Plug in AC plug VERIFICATION: Did the battery indicator LED turn orange? plugin: manual id: led/battery-charged _description: PURPOSE: Validate that the battery LED properly displays charged status STEPS: 1. Let system run on battery for a short time 2. Plug in AC 3. Let system run on AC VERIFICATION: Does the orange battery LED shut off when system is fully charged? plugin: manual id: led/battery-low _description: PURPOSE: Validate that the battery LED indicated low power STEPS: 1. Let system run on battery for several hours 2. Monitor battery LED carefully VERIFICATION: Does the LED light orange when battery is low? plugin: user-interact-verify id: led/hdd command: led_hdd_test _description: PURPOSE: HDD LED verification STEPS: 1. Select "Test" to write and read a temporary file for a few seconds 2. HDD LED should light when writing to/reading from HDD VERIFICATION: Did the HDD LED light? plugin: user-interact-verify id: led/numeric-keypad command: keyboard_test _description: PURPOSE: Numeric keypad LED verification STEPS: 1. Press "Block Num" key to toggle numeric keypad LED 2. Click on the "Test" button to open a window to verify your typing 3. Type using the numeric keypad both when the LED is on and off VERIFICATION: 1. Numeric keypad LED status should toggle everytime the "Block Num" key is pressed 2. Numbers should only be entered in the keyboard verification window when the LED is on plugin: manual id: led/caps-lock _description: PURPOSE: Block cap keys LED verification STEPS: 1. Press "Block Cap Keys" to activate/deactivate cap keys blocking 2. Cap Keys LED should be switched on/off every time the key is pressed VERIFICATION: Did the Cap Keys LED light as expected? plugin: manual id: led/wlan depends: keys/wireless _description: PURPOSE: WLAN LED verification STEPS: 1. During the keys/wireless test you should have observed the wireless LED while turning wireless back on. 2. WLAN LED should light or change color when wireless is turned on VERIFICATION: Did the WLAN LED turn on or change color as expected? plugin: manual id: led/wlan-disabled depends: keys/wireless _description: PURPOSE: Validate that WLAN LED shuts off when disabled STEPS: 1. During the keys/wireless test you should have observed the WLAN LED while performing that test after turning wireless off. 2. WLAN LED should turn off or change color when wireless is turned off VERIFICATION: Did the WLAN LED turn off or change color as expected? plugin: manual id: led/bluetooth depends: bluetooth/detect-output _description: PURPOSE: Validate that the Bluetooth LED turns on and off when BT is enabled/disabled STEPS: 1. Switch bluetooth off from a hardware switch (if present) 2. Switch bluetooth back on 3. Switch bluetooth off from the panel applet 4. Switch bluetooth back on VERIFICATION: Did the bluetooth LED turn off and on twice? plugin: user-interact-verify id: led/camera depends: camera/detect command: camera_test led _description: PURPOSE: Camera LED verification STEPS: 1. Select Test to activate camera 2. Camera LED should light for a few seconds VERIFICATION: Did the camera LED light? plugin: manual id: led/touchpad _description: PURPOSE: Touchpad LED verification STEPS: 1. Click on the touchpad button or press key combination to enable/disable touchpad button 2. Slide your finger on the touchpad VERIFICATION: 1. Touchpad LED status should toggle everytime the button is clicked or the key combination is pressed 2. When the LED is on, the mouse pointer should move on touchpad usage 3. When the LED is off, the mouse pointer should not move on touchpad usage plugin: manual id: led/wireless _description: PURPOSE: Wireless (WLAN + Bluetooth) LED verification STEPS: 1. Make sure WLAN connection is established and Bluetooth is enabled. 2. WLAN/Bluetooth LED should light 3. Switch WLAN and Bluetooth off from a hardware switch (if present) 4. Switch them back on 5. Switch WLAN and Bluetooth off from the panel applet 6. Switch them back on VERIFICATION: Did the WLAN/Bluetooth LED light as expected? plugin: manual id: led/mute depends: keys/mute _description: PURPOSE: Audio Mute LED verification. STEPS: Skip this test if your system does not have a special Audio Mute LED. 1. Press the Mute key twice and observe the Audio LED to determine if it either turned off and on or changed colors. VERIFICATION: Did the Audio LED turn on and off change color as expected? 2013.com.canonical.certification.checkbox-0.4/jobs/hibernate.txt.in0000664000175000017500000000305612320567463025104 0ustar zygazyga00000000000000plugin: user-interact-verify id: power-management/hibernate_advanced requires: sleep.disk == 'supported' rtc.state == 'supported' environ: PLAINBOX_SESSION_SHARE user: root command: if type -P fwts >/dev/null; then echo "Calling fwts" fwts_test -l $PLAINBOX_SESSION_SHARE/hibernate-single -f none -s s4 --s4-device-check --s4-device-check-delay=45 --s4-sleep-delay=120 else echo "Calling sleep_test" sleep_test -s disk -w 120 fi estimated_duration: 300.00 _description: PURPOSE: This test will check to make sure your system can successfully hibernate (if supported) STEPS: 1. Click on Test 2. The system will hibernate and should wake itself within 5 minutes 3. If your system does not wake itself after 5 minutes, please press the power button to wake the system manually 4. If the system fails to resume from hibernate, please restart System Testing and mark this test as Failed VERIFICATION: Did the system successfully hibernate and did it work properly after waking up? plugin: shell id: power-management/hibernate-single-log-check command: [ -e $PLAINBOX_SESSION_SHARE/hibernate-single.log ] && sleep_test_log_check -v s4 $PLAINBOX_SESSION_SHARE/hibernate-single.log _description: Automated check of the hibernate log for errors discovered by fwts plugin: attachment id: power-management/hibernate-single-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/hibernate-single.log ] && cat $PLAINBOX_SESSION_SHARE/hibernate-single.log estimated_duration: 0.5 description: attaches log from single hibernate/resume test to results 2013.com.canonical.certification.checkbox-0.4/jobs/miscellanea.txt.in0000664000175000017500000001262712320567463025424 0ustar zygazyga00000000000000plugin: shell id: miscellanea/submission-resources depends: cpuinfo dmi dpkg lsb package requirements uname dmi_attachment sysfs_attachment udev_attachment estimated_duration: 1.0 command: true _description: A meta-job depending on the resources needed for a valid submission. plugin: manual id: miscellanea/tester-info _description: PURPOSE: Keep tester related information in the report STEPS: 1. Tester Information 2. Please enter the following information in the comments field: a. Name b. Email Address c. Reason for this test run VERIFICATION: Nothing to verify for this test plugin: user-interact-verify id: miscellanea/chvt requires: package.name == 'kbd' user: root command: cycle_vts _description: PURPOSE: This test will check that the system can switch to a virtual terminal and back to X STEPS: 1. Click "Test" to switch to another virtual terminal and then back to X VERIFICATION: Did your screen change temporarily to a text console and then switch back to your current session? plugin: shell id: miscellanea/fwts_test estimated_duration: 1.2 requires: package.name == 'fwts' user: root _description: Run Firmware Test Suite (fwts) automated tests. environ: PLAINBOX_SESSION_SHARE command: fwts_test -l $PLAINBOX_SESSION_SHARE/fwts_results.log plugin: attachment id: miscellanea/fwts_results.log command: [[ -e ${PLAINBOX_SESSION_SHARE}/fwts_results.log ]] && cat ${PLAINBOX_SESSION_SHARE}/fwts_results.log _description: Attaches the FWTS results log to the submission plugin: shell id: miscellanea/ipmi_test requires: package.name == 'ipmitool' user: root command: ipmi_test _description: This will run some basic connectivity tests against a BMC, verifying that IPMI works. plugin: shell id: miscellanea/is_laptop user: root _description: Determine if we need to run tests specific to portable computers that may not apply to desktops. command: check_is_laptop plugin: shell id: miscellanea/apport-directory requires: package.name == 'apport' command: if [ -d /var/crash ]; then if [ $(find /var/crash -type f | grep -v .lock | wc -l) -eq 0 ]; then echo "/var/crash is empty"; else echo `ls /var/crash`; false; fi; else echo "/var/crash does not exist"; fi _description: Test that the /var/crash directory doesn't contain anything. Lists the files contained within if it does, or echoes the status of the directory (doesn't exist/is empty) plugin: shell id: miscellanea/sources-list command: sources_test $SOURCES_LIST "$REPOSITORIES" _description: Checks that a specified sources list file contains the requested repositories plugin: local id: miscellanea/pxe_boot requires: device.category == 'NETWORK' _description: Automated job to generate the PXE verification test for each NIC. command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: manual id: miscellanea/pxe_boot_$2 description: PURPOSE: This test will verify that you have attempted to PXE boot this machine from the network device $2. STEPS: 1. Prior to running this test, you should have attempted to boot this system via PXE on every Network Port available. VERIFICATION: 1. Select Yes if you successfully used PXE boot this system using the NIC $2 2. Select No if you did not attempt to PXE boot this system using the NIC $2 3. Select No if you attempted to PXE boot via $2 and it failed for some reason. EOF plugin: local id: miscellanea/remote_shared_ipmi requires: device.category == 'NETWORK' _description: Automated job to generate the Remote Shared IPMI verification test for each NIC. command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: manual id: miscellanea/remote_shared_ipmi_$2 description: PURPOSE: This test will verify that you have attempted IPMI control of this machine from the network device $2. STEPS: 1. Prior to running this test, you should have configured and used IPMI to power this machine off and on using every Network Port available. VERIFICATION: 1. Select Yes if you successfully used IPMI to remotely power this system off and on using the NIC $2 2. Select No if you did not attempt to use IPMI to remotely power this sytem off and on via the NIC $2 3. Select No if you attempted to use IPMI to remotely power off/on this system via $2 and it failed for some reason. EOF plugin: manual id: miscellanea/remote_dedicated_ipmi _description: PURPOSE: Some systems do not share IPMI over all NICs but instead have a dedicated management port directly connected to the BMC. This test verifies that you have used that port for remote IPMI connections and actions. STEPS: 1. Prior to running the test, you should have configured and used the Dedicated Management Port to remotely power off/on this sytem. VERIFICATION: Skip this test if this system ONLY uses shared management/ethernet ports OR if this system does not have a BMC (Management Console) 1. Select Yes if you successfully used IPMI to remotely power this system off and on using the dedicated management port. 2. Select No if you attempted to use the dedicated management port to remotely power this system off/on and it failed for some reason. 2013.com.canonical.certification.checkbox-0.4/jobs/networking.txt.in0000664000175000017500000000502012320565736025324 0ustar zygazyga00000000000000plugin: shell id: networking/gateway_ping depends: ethernet/detect command: gateway_ping_test estimated_duration: 2.000 _description: Tests whether the system has a working Internet connection. plugin: local id: networking/info requires: device.category == 'NETWORK' _description: Network Information command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: user-interact-verify id: networking/info_$2 requires: device.path == "$1" command: network_info $2 | zenity --text-info --title="$2" _description: PURPOSE: This test will check the $2 network interface STEPS: 1. Click "Test" to verify the information for $2 VERIFICATION: Is this correct? EOF plugin: user-interact-verify id: networking/modem_connection command: network_check _description: PURPOSE: This test will check that a DSL modem can be configured and connected. STEPS: 1. Connect the telephone line to the computer 2. Click on the Network icon on the top panel. 3. Select "Edit Connections" 4. Select the "DSL" tab 5. Click on "Add" button 6. Configure the connection parameters properly 7. Click "Test" to verify that it's possible to establish an HTTP connection VERIFICATION: Did a notification show and was the connection correctly established? plugin: shell id: networking/ping command: gateway_ping_test $CHECKBOX_SERVER _description: Automated test case to verify availability of some system on the network using ICMP ECHO packets. plugin: shell id: networking/http command: wget -SO /dev/null http://$TRANSFER_SERVER _description: Automated test case to make sure that it's possible to download files through HTTP plugin: shell id: networking/ntp requires: package.name == 'ntpdate' user: root command: network_ntp_test _description: Test to see if we can sync local clock to an NTP server plugin: shell id: networking/ssh requires: package.name == 'openssh-client' command: if [ $CHECKBOX_SERVER ]; then ssh -q -o 'StrictHostKeyChecking=no' -o "UserKnownHostsFile=/tmp/ssh_test_$$" -l ubuntu $CHECKBOX_SERVER "uname -a" && rm /tmp/ssh_test_$$; fi _description: Verify that an installation of checkbox-server on the network can be reached over SSH. plugin: shell id: networking/printer requires: package.name == 'cups-client' command: network_printer_test -s $CHECKBOX_SERVER _description: Try to enable a remote printer on the network and print a test page. 2013.com.canonical.certification.checkbox-0.4/jobs/touchpad.txt.in0000664000175000017500000001161412320565736024752 0ustar zygazyga00000000000000plugin: manual id: touchpad/basic requires: dmi.product in ['Notebook','Laptop','Portable'] estimated_duration: 120.0 _description: PURPOSE: Touchpad user-verify STEPS: 1. Make sure that touchpad is enabled. 2. Move cursor using the touchpad. VERIFICATION: Did the cursor move? plugin: user-interact id: touchpad/horizontal requires: dmi.product in ['Notebook','Laptop','Portable'] 'Button Horiz Wheel Left' in xinput.button_labels and 'Button Horiz Wheel Right' in xinput.button_labels command: touchpad_test right left --edge-scroll estimated_duration: 120.0 _description: PURPOSE: Touchpad horizontal scroll verification STEPS: 1. Select "Test" when ready and place your cursor within the borders of the displayed test window. 2. Verify that you can move the horizontal slider by moving your finger right and left in the lower part of the touchpad. VERIFICATION: Could you scroll right and left? plugin: user-interact id: touchpad/vertical requires: dmi.product in ['Notebook','Laptop','Portable'] 'Button Wheel Up' in xinput.button_labels and 'Button Wheel Down' in xinput.button_labels command: touchpad_test up down --edge-scroll estimated_duration: 120.0 _description: PURPOSE: Touchpad vertical scroll verification STEPS: 1. Select "Test" when ready and place your cursor within the borders of the displayed test window. 2. Verify that you can move the vertical slider by moving your finger up and down in the right part of the touchpad. VERIFICATION: Could you scroll up and down? plugin: shell id: touchpad/singletouch-automated requires: dmi.product in ['Notebook','Laptop','Portable'] xinput.device_class == 'XITouchClass' and xinput.touch_mode != 'dependent' command: true estimated_duration: 1.2 _description: Determine whether the touchpad is detected as a singletouch device automatically. plugin: shell id: touchpad/multitouch-automated estimated_duration: 1.2 requires: dmi.product in ['Notebook','Laptop','Portable'] xinput.device_class == 'XITouchClass' and xinput.touch_mode == 'dependent' command: true _description: Determine whether the touchpad is detected as a multitouch device automatically. plugin: manual id: touchpad/multitouch-manual depends: touchpad/singletouch-automated estimated_duration: 120.0 _description: PURPOSE: Touchpad manual detection of multitouch. STEPS: 1. Look at the specifications for your system. VERIFICATION: Is the touchpad supposed to be multitouch? plugin: manual id: touchpad/singletouch-corner estimated_duration: 120.0 _description: PURPOSE: Determine that the hot corner feature is working as expected STEPS: 1. Launch a browser. 2. Go to a website, and hover the cursor over a link. 3. Tap the upper right corner if the touchpad. VERIFICATION: Did the tap trigger a new tab to be opended? plugin: manual id: touchpad/singletouch-selection estimated_duration: 120.0 _description: PURPOSE: Determine that the selection window function is working as expected. STEPS: 1. Open a file folder 2. Double tap and drag the cursor across several file. VERIFICATION: Did a selection window open and were several files selected? plugin: manual id: touchpad/multitouch-rightclick depends: touchpad/multitouch-automated estimated_duration: 120.0 _description: PURPOSE: Determine that the right click function is working as expected. STEPS: 1. Open a file folder 2. Hover cursor over file in folder 3. 2-touch tap. VERIFICATION: Did the right click pop up menu appear? plugin: user-interact id: touchpad/multitouch-horizontal command: touchpad_test right left estimated_duration: 120.0 _description: PURPOSE: Touchpad 2-touch horizontal scroll verification STEPS: 1. Select "Test" when ready and place your cursor within the borders of the displayed test window. 2. Verify that you can move the horizontal slider by moving 2 fingers right and left along the touchpad. VERIFICATION: Could you scroll right and left? plugin: user-interact id: touchpad/multitouch-vertical command: touchpad_test up down estimated_duration: 120.0 _description: PURPOSE: Touchpad 2-touch vertical scroll verification STEPS: 1. Select "Test" when ready and place your cursor within the borders of the displayed test window. 2. Verify that you can move the vertical slider by moving 2 fingers up and down along the touchpad. VERIFICATION: Could you scroll up and down? plugin: manual id: touchpad/drag-and-drop estimated_duration: 120.0 _description: PURPOSE: Determine that the drag and drop function is working as expected. STEPS: 1. Browse to the examples folder in the current user's home directory 2. Double tap and hold to select the "Ubuntu_Free_Culture_Showcase" folder 2. Drag the selected folder to the desktop and remove finger from touchpad. VERIFICATION: Did a selected folder move to the desktop? 2013.com.canonical.certification.checkbox-0.4/jobs/codecs.txt.in0000664000175000017500000000202212320565736024374 0ustar zygazyga00000000000000plugin: user-interact-verify id: codecs/ogg-vorbis requires: package.name == 'gstreamer0.10-plugins-base' or package.name == 'gstreamer1.0-plugins-base' package.name == 'totem' package.name == 'ubuntu-sounds' command: totem /usr/share/sounds/ubuntu/stereo/system-ready.ogg _description: PURPOSE: This test will verify your system's ability to play Ogg Vorbis audio files. STEPS: 1. Click Test to play an Ogg Vorbis file (.ogg) 2. Please close the player to proceed. VERIFICATION: Did the sample play correctly? plugin: user-interact-verify id: codecs/wav requires: package.name == 'gstreamer0.10-plugins-good' or package.name == 'gstreamer1.0-plugins-good' package.name == 'totem' package.name == 'alsa-utils' command: totem /usr/share/sounds/alsa/Noise.wav _description: PURPOSE: This test will verify your system's ability to play Wave Audio files. STEPS: 1. Select Test to play a Wave Audio format file (.wav) 2. Please close the player to proceed. VERIFICATION: Did the sample play correctly? 2013.com.canonical.certification.checkbox-0.4/jobs/power-management.txt.in0000664000175000017500000001666012320567463026416 0ustar zygazyga00000000000000plugin: manual id: power-management/shutdown-boot _description: PURPOSE: This test will check your system shutdown/booting cycle. STEPS: 1. Shutdown your machine. 2. Boot your machine. 3. Repeat steps 1 and 2 at least 5 times. VERIFICATION: Did the system shutdown and rebooted correctly? plugin: shell id: power-management/fwts_wakealarm environ: PLAINBOX_SESSION_SHARE user: root _description: Test ACPI Wakealarm (fwts wakealarm) requires: package.name == 'fwts' command: fwts_test -f aborted -t wakealarm -l $PLAINBOX_SESSION_SHARE/fwts-wakealarm.log plugin: attachment id: power-management/fwts_wakealarm-log-attach depends: power-management/fwts_wakealarm _description: Attach log from fwts wakealarm test requires: package.name == 'fwts' command: [ -e ${PLAINBOX_SESSION_SHARE}/fwts-wakealarm.log ] && cat ${PLAINBOX_SESSION_SHARE}/fwts-wakealarm.log plugin: user-interact-verify id: power-management/poweroff depends: power-management/fwts_wakealarm user: root environ: PLAINBOX_SESSION_SHARE requires: package.name == 'upstart' package.name == 'fwts' command: pm_test poweroff --log-level=debug --log-dir=$PLAINBOX_SESSION_SHARE _description: PURPOSE: This test will check the system's ability to power-off and boot. STEPS: 1. Select "Test" to begin. 2. The machine will shut down. 3. Power the machine back on. 4. After rebooting, wait for the test prompts to inform you that the test is complete. 5. Once the test has completed, restart checkbox and select 'Re-run' when prompted. VERIFICATION: If the machine successfully shuts down and boots, select 'Yes', otherwise, select 'No'. plugin: attachment id: power-management/poweroff-log-attach command: tar cvfz power-management_poweroff.tgz $PLAINBOX_SESSION_SHARE/*poweroff.1.log && cat $PLAINBOX_SESSION_SHARE/power-management_poweroff.tgz _description: This will attach any logs from the power-management/poweroff test to the results. plugin: user-interact-verify id: power-management/reboot user: root environ: PLAINBOX_SESSION_SHARE requires: package.name == 'upstart' package.name == 'fwts' command: pm_test reboot --log-level=debug --log-dir=$PLAINBOX_SESSION_SHARE _description: PURPOSE: This test will check the system's ability to reboot cleanly. STEPS: 1. Select "Test" to begin. 2. The machine will reboot. 3. After rebooting, wait for the test prompts to inform you that the test is complete. 4. Once the test has completed, restart checkbox and select Re-Run when prompted. VERIFICATION: If the machine successfully reboots, select Yes then select Next. plugin: attachment id: power-management/reboot-log-attach command: tar cvfz power-management_reboot.tgz $PLAINBOX_SESSION_SHARE/*reboot.1.log && cat $PLAINBOX_SESSION_SHARE/power-management_reboot.tgz _description: This will attach any logs from the power-management/reboot test to the results. plugin: manual id: power-management/lid requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will check your lid sensors. STEPS: 1. Close your laptop lid. VERIFICATION: Does closing your laptop lid cause your system to suspend? plugin: user-interact id: power-management/lid_close requires: device.product == 'Lid Switch' command: for i in `seq 20`; do state=`cat /proc/acpi/button/lid/*/state | awk '{print $2}'` [ "$state" = "closed" ] && exit 0 || sleep 0.5 done exit 1 _description: PURPOSE: This test will check your lid sensors STEPS: 1. Click "Test". 2. Close and open the lid. VERIFICATION: Did the screen turn off while the lid was closed? plugin: user-interact id: power-management/lid_open requires: device.product == 'Lid Switch' command: for i in `seq 20`; do state=`cat /proc/acpi/button/lid/*/state | awk '{print $2}'` [ "$state" = "open" ] && exit 0 || sleep 0.5 done exit 1 _description: PURPOSE: This test will check your lid sensors. STEPS: 1. Click "Test". 2. Close the lid. 3. Wait 5 seconds with the lid closed. 4. Open the lid. VERIFICATION: Did the system resume when the lid was opened? plugin: shell id: power-management/rtc requires: rtc.state == 'supported' package.name == 'util-linux' user: root command: hwclock -r estimated_duration: 0.02 _description: Verify that the Real-time clock (RTC) device functions properly, if present plugin: shell id: power-management/tickless_idle _description: Check to see if CONFIG_NO_HZ is set in the kernel (this is just a simple regression check) command: zgrep 'CONFIG_NO_HZ=y' /boot/config-`uname -r` >/dev/null 2>&1 || ( echo "WARNING: Tickless Idle is NOT set" >&2 && exit 1 ) plugin: manual id: power-management/unplug_ac _description: PURPOSE: This test will ensure that the AC is unplugged for the battery drain tests to run. STEPS: 1. Unplug laptop from AC. VERIFICATION: Was the laptop unplugged from AC? plugin: shell id: power-management/battery_drain_idle requires: package.name == 'upower' depends: power-management/unplug_ac _description: Checks the battery drain during idle. Reports time until empty and capacity as well. command: battery_test -t 90 --idle plugin: shell id: power-management/battery_drain_movie requires: package.name == 'upower' depends: power-management/unplug_ac _description: Checks the battery drain while watching a movie. Reports time until empty and capacity as well. Requires MOVIE_VAR to be set. command: battery_test -t 90 --movie $MOVIE_VAR plugin: shell id: power-management/battery_drain_sleep user: root requires: package.name == 'upower' package.name == 'fwts' depends: power-management/unplug_ac _description: Checks the battery drain during suspend. Reports time until empty and capacity as well. command: battery_test -t 120 --sleep plugin: manual id: power-management/plug_ac depends: power-management/battery_drain_idle power-management/battery_drain_movie power-management/battery_drain_sleep _description: PURPOSE: This test will ensure that the AC is plugged back in after the battery. tests STEPS: 1. Plug laptop into AC. VERIFICATION: Was the laptop plugged into AC? plugin: user-interact-verify id: power-management/reboot_manual user:root command: shutdown -r now _description: PURPOSE: This test will verify that your system can successfully reboot. STEPS: 1. Select 'Test' to initiate a system reboot. 2. When the grub boot menu is displayed, boot into Ubuntu (Or allow the system to automatically boot on its own). 3. Once the system has restarted, log in and restart checkbox-certification-server. 4. Select 'Re-Run' to return to this test. 5. Select 'Yes' to indicate the test has passed if the system rebooted successfully, otherwise, select 'No' to indicate there was a problem. VERIFICATION: Did the system reboot correctly? plugin: user-interact-verify id: power-management/shutdown_manual user: root command: shutdown -h now _description: PURPOSE: This test will check your system shutdown/booting cycle STEPS: 1. Select 'Test' to initiate a system shutdown. 2. Power the system back on. 3. From the grub menu, boot into the Xen Hypervisor. 4. When the system has restarted, log in and restart checkbox-certification-server. 5. Select 'Re-Run' to return to this test. 6. Select 'Yes' to indicate the test has passed if the machine shut down successfully otherwise, Select 'No' to indicate there was a problem. VERIFICATION: Did the system shutdown and boot correctly? 2013.com.canonical.certification.checkbox-0.4/jobs/suspend.txt.in0000664000175000017500000022627312320567463024634 0ustar zygazyga00000000000000plugin: shell id: suspend/network_before_suspend depends: ethernet/detect estimated_duration: 1.2 _description: Record the current network before suspending. command: set -o pipefail; gateway_ping_test | tee $PLAINBOX_SESSION_SHARE/network_before_suspend.txt plugin: shell id: suspend/resolution_before_suspend estimated_duration: 1.2 _description: Record the current resolution before suspending. command: xrandr -q |grep '*'| awk '{print $1}' > $PLAINBOX_SESSION_SHARE/resolution_before_suspend.txt plugin: shell id: suspend/audio_before_suspend estimated_duration: 1.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' _description: Record mixer settings before suspending. command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/audio_settings_before_suspend plugin: shell id: suspend/cpu_before_suspend estimated_duration: 1.2 _description: Verify that all the CPUs are online before suspending command: cpuinfo_resource > $PLAINBOX_SESSION_SHARE/cpuinfo_before_suspend plugin: shell id: suspend/memory_before_suspend estimated_duration: 1.2 _description: Dumps memory info to a file for comparison after suspend test has been run command: meminfo_resource > $PLAINBOX_SESSION_SHARE/meminfo_before_suspend plugin: shell id: suspend/wireless_before_suspend depends: wireless/wireless_connection requires: device.category == 'WIRELESS' command: nmcli -t -f UUID con status > $PLAINBOX_SESSION_SHARE/connections && connect_wireless && gateway_ping_test --interface=`nmcli dev list | grep -B 1 wireless | grep GENERAL.DEVICE | awk '{print $2}'` && for con in `cat $PLAINBOX_SESSION_SHARE/connections`; do nmcli con up uuid "$con"; done estimated_duration: 20.0 _description: This test disconnects all connections and then connects to the wireless interface. It then checks the connection to confirm it's working as expected. plugin: local id: suspend/iperf_before_suspend_ether_auto requires: device.category == 'NETWORK' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: suspend/iperf_before_suspend_ether_auto_$2 depends: ethernet/detect estimated_duration: 20.0 requires: device.path == "$1" user: root command: network -i $2 -t iperf _description: This test executes iperf connection performance/stability against all the ethernet devices found on the system before suspend. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: local id: suspend/iperf_before_suspend_wifi_auto requires: device.category == 'WIRELESS' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=WIRELESS" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: suspend/iperf_before_suspend_wifi_auto_$2 depends: wireless/wireless_connection estimated_duration: 20.0 requires: device.path == "$1" user: root command: network -i $2 -t iperf _description: This test executes iperf connection performance/stability against all the ethernet devices found on the system before suspend. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: shell id: suspend/bluetooth_obex_before_suspend estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexd-client' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill shows BT is soft blocked, removing before testing." rfkill unblock bluetooth sleep 3 fi obex_send $BTDEVADDR $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_send_before_suspend estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR send 2>&1 | ansi_parser _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_browse_before_suspend estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR browse 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It emulates browsing on a remote device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_get_before_suspend estimated_duration: 20.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR get 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It receives the given file from a remote host specified by the BTDEVADDR environment variable plugin: user-interact-verify id: suspend/bluetooth_obex_before_suspend_manual estimated_duration: 120.0 requires: package.name == 'bluez' package.name == 'obexd-client' device.category == 'BLUETOOTH' command: rfkill unblock bluetooth; obex_send `bluetooth_scan` $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg _description: PURPOSE: This test will send the image 'JPEG_Color_Image_Ubuntu.jpg' to a specified device STEPS: 1. Make sure Bluetooth is enabled by checking the Bluetooth indicator applet 2. Click "Test" and you will be prompted to enter the Bluetooth device name of a device that can accept file transfers (It may take a few moments after entering the name for the file to begin sending) 3. Accept any prompts that appear on both devices VERIFICATION: Was the data correctly transferred? plugin: user-verify id: suspend/suspend_advanced requires: sleep.mem == 'supported' rtc.state == 'supported' user: root environ: PLAINBOX_SESSION_SHARE command: if type -P fwts >/dev/null; then echo "Calling fwts" set -o pipefail; fwts_test -f none -l $PLAINBOX_SESSION_SHARE/suspend_single -s s3 --s3-sleep-delay=30 --s3-device-check --s3-device-check-delay=45 | tee $PLAINBOX_SESSION_SHARE/suspend_single_times.log else echo "Calling sleep_test" set -o pipefail; sleep_test -p | tee $PLAINBOX_SESSION_SHARE/suspend_single_times.log fi estimated_duration: 90.0 _description: PURPOSE: This test will check suspend and resume STEPS: 1. Click "Test" and your system will suspend for about 30 - 60 seconds 2. Observe the Power LED to see if it blinks or changes color during suspend 3. If your system does not wake itself up after 60 seconds, please press the power button momentarily to wake the system manually 4. If your system fails to wake at all and must be rebooted, restart System Testing after reboot and mark this test as Failed VERIFICATION: Did your system suspend and resume correctly? (NOTE: Please only consider whether the system successfully suspended and resumed. Power/Suspend LED verification will occur after this test is completed.) plugin: shell id: suspend/suspend_advanced_auto requires: sleep.mem == 'supported' rtc.state == 'supported' _description: This is the automated version of suspend/suspend_advanced. user: root environ: PLAINBOX_SESSION_SHARE command: set -o pipefail; fwts_test -f none -l $PLAINBOX_SESSION_SHARE/suspend_single -s s3 --s3-sleep-delay=30 --s3-device-check --s3-device-check-delay=45 | tee $PLAINBOX_SESSION_SHARE/suspend_single_times.log estimated_duration: 90.000 plugin: shell id: suspend/suspend-single-log-check estimated_duration: 1.2 command: [ -e $PLAINBOX_SESSION_SHARE/suspend_single.log ] && sleep_test_log_check -v s3 $PLAINBOX_SESSION_SHARE/suspend_single.log _description: Automated check of the suspend log to look for errors reported by fwts plugin: attachment id: suspend/suspend-single-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/suspend_single.log ] && cat $PLAINBOX_SESSION_SHARE/suspend_single.log _description: Attaches the log from the single suspend/resume test to the results plugin: shell id: suspend/suspend-time-check estimated_duration: 1.2 command: [ -e $PLAINBOX_SESSION_SHARE/suspend_single_times.log ] && sleep_time_check $PLAINBOX_SESSION_SHARE/suspend_single_times.log _description: Checks the sleep times to ensure that a machine suspends and resumes within a given threshold plugin: user-interact-verify id: power-management/usb_wakeup_keyboard user: root depends: suspend/suspend_advanced estimated_duration: 120.0 command: pm-suspend _description: PURPOSE: Wake up by USB keyboard STEPS: 1. Enable "Wake by USB KB/Mouse" item in BIOS 2. Press "Test" to enter suspend (S3) mode 3. Press any key of USB keyboard to wake system up VERIFICATION: Did the system wake up from suspend mode when you pressed a keyboard key? plugin: user-interact-verify id: power-management/usb_wakeup_mouse user: root depends: suspend/suspend_advanced estimated_duration: 120.0 command: pm-suspend _description: PURPOSE: Wake up by USB mouse STEPS: 1. Enable "Wake by USB KB/Mouse" item in BIOS 2. Press "Test" to enter suspend (S3) mode 3. Press any button of USB mouse to wake system up VERIFICATION: Did the system wake up from suspend mode when you pressed the mouse button? plugin: shell id: suspend/network_after_suspend estimated_duration: 20.0 depends: suspend/suspend_advanced suspend/network_before_suspend _description: Test the network after resuming. command: network_wait; gateway_ping_test | diff $PLAINBOX_SESSION_SHARE/network_before_suspend.txt - plugin: shell id: suspend/resolution_after_suspend depends: suspend/suspend_advanced suspend/resolution_before_suspend estimated_duration: 1.2 _description: Test to see that we have the same resolution after resuming as before. command: xrandr -q |grep '*'| awk '{print $1}' | diff $PLAINBOX_SESSION_SHARE/resolution_before_suspend.txt - plugin: shell id: suspend/audio_after_suspend estimated_duration: 1.0 requires: device.category == 'AUDIO' package.name == 'alsa-base' depends: suspend/suspend_advanced suspend/audio_before_suspend _description: Verify that mixer settings after suspend are the same as before suspend. command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/audio_settings_after_suspend; diff $PLAINBOX_SESSION_SHARE/audio_settings_before_suspend $PLAINBOX_SESSION_SHARE/audio_settings_after_suspend plugin: shell id: suspend/audio_after_suspend_auto estimated_duration: 1.2 requires: device.category == 'AUDIO' package.name == 'alsa-base' depends: suspend/suspend_advanced_auto suspend/audio_before_suspend _description: Verify that mixer settings after suspend are the same as before suspend. command: audio_settings store --file=$PLAINBOX_SESSION_SHARE/audio_settings_after_suspend; diff $PLAINBOX_SESSION_SHARE/audio_settings_before_suspend $PLAINBOX_SESSION_SHARE/audio_settings_after_suspend plugin: shell id: suspend/cpu_after_suspend estimated_duration: 1.2 depends: suspend/suspend_advanced suspend/cpu_before_suspend _description: Verify that all CPUs are online after resuming. command: cpuinfo_resource | diff $PLAINBOX_SESSION_SHARE/cpuinfo_before_suspend - plugin: shell id: suspend/cpu_after_suspend_auto estimated_duration: 1.2 depends: suspend/suspend_advanced_auto suspend/cpu_before_suspend _description: Verify that all CPUs are online after resuming. command: cpuinfo_resource | diff $PLAINBOX_SESSION_SHARE/cpuinfo_before_suspend - plugin: shell id: suspend/memory_after_suspend estimated_duration: 1.2 depends: suspend/suspend_advanced suspend/memory_before_suspend _description: Verify that all memory is available after resuming from suspend. command: meminfo_resource | diff $PLAINBOX_SESSION_SHARE/meminfo_before_suspend - plugin: shell id: suspend/memory_after_suspend_auto estimated_duration: 1.2 depends: suspend/suspend_advanced_auto suspend/memory_before_suspend _description: Verify that all memory is available after resuming from suspend. command: meminfo_resource | diff $PLAINBOX_SESSION_SHARE/meminfo_before_suspend - plugin: manual id: suspend/display_after_suspend estimated_duration: 120.0 depends: suspend/suspend_advanced _description: PURPOSE: This test will check that the display is correct after suspend and resume STEPS: 1. Check that your display does not show up visual artifacts after resuming. VERIFICATION: Does the display work normally after resuming from suspend? plugin: shell id: suspend/wireless_after_suspend depends: suspend/suspend_advanced suspend/wireless_before_suspend requires: device.category == 'WIRELESS' command: connect_wireless && gateway_ping_test --interface=`nmcli dev list | grep -B 1 wireless | grep GENERAL.DEVICE | awk '{print $2}'` && for con in `cat $PLAINBOX_SESSION_SHARE/connections`; do nmcli con up uuid "$con"; done estimated_duration: 20.0 _description: This test checks that the wireless interface is working after suspending the system. It disconnects all interfaces and then connects to the wireless interface and checks that the connection is working as expected. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_bg depends: suspend/suspend_advanced estimated_duration: 20.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: WPA_BG_SSID WPA_BG_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_BG_SSID" EXIT; create_connection wifi $WPA_BG_SSID --security=wpa --key=$WPA_BG_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11b/g protocols after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_bg depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: OPEN_BG_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_BG_SSID" EXIT; create_connection wifi $OPEN_BG_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11b/g protocols after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_n depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.n == 'supported' user: root environ: WPA_N_SSID WPA_N_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_N_SSID" EXIT; create_connection wifi $WPA_N_SSID --security=wpa --key=$WPA_N_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11n protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_n depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.n == 'supported' user: root environ: OPEN_N_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_N_SSID" EXIT; create_connection wifi $OPEN_N_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11n protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_ac depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.ac == 'supported' user: root environ: WPA_AC_SSID WPA_AC_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_AC_SSID" EXIT; create_connection wifi $WPA_AC_SSID --security=wpa --key=$WPA_AC_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11ac protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_ac depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' IEEE_80211.ac == 'supported' user: root environ: OPEN_AC_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_AC_SSID" EXIT; create_connection wifi $OPEN_AC_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11ac protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_bg_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: WPA_BG_SSID WPA_BG_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_BG_SSID" EXIT; create_connection wifi $WPA_BG_SSID --security=wpa --key=$WPA_BG_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11b/g protocols after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_bg_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: OPEN_BG_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_BG_SSID" EXIT; create_connection wifi $OPEN_BG_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11b/g protocols after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_n_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: WPA_N_SSID WPA_N_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_N_SSID" EXIT; create_connection wifi $WPA_N_SSID --security=wpa --key=$WPA_N_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11n protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_n_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: OPEN_N_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_N_SSID" EXIT; create_connection wifi $OPEN_N_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11n protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_wpa_ac_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: WPA_AC_SSID WPA_AC_PSK command: trap "rm -f /etc/NetworkManager/system-connections/$WPA_AC_SSID" EXIT; create_connection wifi $WPA_AC_SSID --security=wpa --key=$WPA_AC_PSK; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11ac protocol after the system has been suspended. plugin: shell id: suspend/wireless_connection_after_suspend_open_ac_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'WIRELESS' environment.ROUTERS == 'multiple' user: root environ: OPEN_AC_SSID command: trap "rm -f /etc/NetworkManager/system-connections/$OPEN_AC_SSID" EXIT; create_connection wifi $OPEN_AC_SSID; gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: Tests that the systems wireless hardware can connect to a router using no security and the 802.11ac protocol after the system has been suspended. plugin: local id: suspend/iperf_after_suspend_ether_auto estimated_duration: 30.0 requires: device.category == 'NETWORK' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=NETWORK" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: suspend/iperf_after_suspend_ether_auto_$2 depends: suspend/suspend_advanced requires: device.path == "$1" user: root command: network -i $2 -t iperf _description: This test executes iperf connection performance/stability against all the ethernet devices found on the system before suspend. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: local id: suspend/iperf_after_suspend_wifi_auto estimated_duration: 30.0 requires: device.category == 'WIRELESS' command: cat <<'EOF' | run_templates -s 'udev_resource | filter_templates -w "category=WIRELESS" | awk "/path: / { print \$2 }" | xargs -n 1 sh -c "for i in \`ls /sys\$0/net 2>/dev/null\`; do echo \$0 \$i; done"' plugin: shell id: suspend/iperf_before_suspend_wifi_auto_$2 depends: suspend/suspend_advanced requires: device.path == "$1" user: root command: network -i $2 -t iperf _description: This test executes iperf connection performance/stability against all the ethernet devices found on the system before suspend. EOF _description: This is an automated test to gather some info on the current state of your network devices. If no devices are found, the test will exit with an error. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_wpa_bg_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11b/g protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the B and G wireless bands 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_open_bg_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11b/g protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the B and G wireless bands 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_wpa_n_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11n protocols. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the N wireless band 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_open_n_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11n protocol. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the N wireless band 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_wpa_ac_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID ROUTER_PSK command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID --security=wpa --key=$ROUTER_PSK && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using WPA security and the 802.11ac protocol. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the 802.11ac protocol. 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use WPA2 and ensure the PSK matches that set in ROUTER_PSK 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: user-interact-verify id: suspend/wireless_connection_after_suspend_open_ac_manual depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'WIRELESS' environment.ROUTERS == 'single' user: root environ: ROUTER_SSID command: trap "nmcli con delete id $ROUTER_SSID" EXIT; create_connection wifi $ROUTER_SSID && gateway_ping_test --interface=`nmcli dev status | awk '/802-11-wireless/ {print $1}'` _description: PURPOSE: Tests that the systems wireless hardware can connect to a router using no security and the 802.11ac protocol. STEPS: 1. Open your routers configuration tool 2. Change the settings to only accept connections on the 802.11ac protocol. 3. Make sure the SSID is set to ROUTER_SSID 4. Change the security settings to use no security 5. Click the 'Test' button to create a connection to the router and test the connection VERIFICATION: Verification is automated, do not change the automatically selected result. plugin: shell id: suspend/bluetooth_detect_after_suspend depends: suspend/suspend_advanced bluetooth/detect-output estimated_duration: 1.2 requires: package.name == 'bluez' device.category == 'BLUETOOTH' command: if rfkill list bluetooth | grep -q 'Hard blocked: yes'; then echo "rfkill shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes'; then echo "rfkill shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi output=$(hcitool dev | tail -n+2 | awk '{print $2}') echo $output | diff $PLAINBOX_SESSION_SHARE/bluetooth_address - if [ -z "$output" ]; then echo "BT hardware not available" exit 1 fi _description: This test grabs the hardware address of the bluetooth adapter after suspend and compares it to the address grabbed before suspend. plugin: shell id: suspend/bluetooth_obex_after_suspend depends: suspend/suspend_advanced suspend/bluetooth_obex_before_suspend estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexd-client' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill shows BT is soft blocked, removing before testing." rfkill unblock bluetooth sleep 3 fi obex_send $BTDEVADDR $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_after_suspend_auto depends: suspend/suspend_advanced_auto suspend/bluetooth_obex_before_suspend estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexd-client' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill shows BT is soft blocked, removing before testing." rfkill unblock bluetooth sleep 3 fi obex_send $BTDEVADDR $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_send_after_suspend depends: suspend/suspend_advanced estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR send 2>&1 | ansi_parser _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_send_after_suspend_auto depends: suspend/suspend_advanced_auto estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR send 2>&1 | ansi_parser _description: This is an automated Bluetooth file transfer test. It sends an image to the device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_browse_after_suspend depends: suspend/suspend_advanced estimated_duration: 10.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR browse 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It emulates browsing on a remote device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_browse_after_suspend_auto depends: suspend/suspend_advanced_auto estimated_duration: 20.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR browse 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It emulates browsing on a remote device specified by the BTDEVADDR environment variable. plugin: shell id: suspend/bluetooth_obex_get_after_suspend estimated_duration: 20.0 depends: suspend/suspend_advanced requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR get 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It receives the given file from a remote host specified by the BTDEVADDR environment variable plugin: shell id: suspend/bluetooth_obex_get_after_suspend_auto depends: suspend/suspend_advanced_auto estimated_duration: 20.0 requires: package.name == 'bluez' package.name == 'obexftp' device.category == 'BLUETOOTH' command: if [ -z "$BTDEVADDR" ] then echo "btdevaddr option not set to device address of Bluetooth target in checkbox.ini" exit 1 fi if rfkill list bluetooth | grep -q 'Hard blocked: yes' then echo "rfkill list shows BT is hard blocked" fi if rfkill list bluetooth | grep -q 'Soft blocked: yes' then echo "rfkill list shows BT is soft blocked, removing before testing" rfkill unblock bluetooth sleep 3 fi set -o pipefail; bluetooth_test $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg $BTDEVADDR get 2>&1 | ansi_parser _description: This is an automated Bluetooth test. It receives the given file from a remote host specified by the BTDEVADDR environment variable plugin: user-interact-verify id: suspend/bluetooth_obex_after_suspend_manual depends: suspend/suspend_advanced suspend/bluetooth_obex_before_suspend_manual estimated_duration: 120.0 requires: package.name == 'bluez' package.name == 'obexd-client' device.category == 'BLUETOOTH' command: rfkill unblock bluetooth; obex_send `bluetooth_scan` $PLAINBOX_PROVIDER_DATA/images/JPEG_Color_Image_Ubuntu.jpg _description: PURPOSE: This test will send the image 'JPEG_Color_Image_Ubuntu.jpg' to a specified device STEPS: 1. Click "Test" and you will be prompted to enter the Bluetooth device name of a device that can accept file transfers (It may take a few moments after entering the name for the file to begin sending) 2. Accept any prompts that appear on both devices VERIFICATION: Was the data correctly transferred? plugin: user-interact-verify id: suspend/cycle_resolutions_after_suspend estimated_duration: 120.0 requires: package.name == 'xorg' depends: suspend/suspend_advanced graphics/cycle_resolution command: xrandr_cycle --keyword=after_suspend --screenshot-dir $PLAINBOX_SESSION_SHARE _description: PURPOSE: This test will cycle through the detected display modes STEPS: 1. Click "Test" and the display will cycle trough the display modes VERIFICATION: Did your display look fine in the detected mode? plugin: shell id: suspend/cycle_resolutions_after_suspend_auto estimated_duration: 1.2 requires: package.name == 'xorg' depends: suspend/suspend_advanced_auto graphics/cycle_resolution _description: This test will check to make sure supported video modes work after a suspend and resume. This is done automatically by taking screenshots and uploading them as an attachment. command: xrandr_cycle --keyword=after_suspend --screenshot-dir $PLAINBOX_SESSION_SHARE plugin: attachment id: suspend/xrandr_screens_after_suspend.tar.gz depends: suspend/cycle_resolutions_after_suspend_auto command: [ -e $PLAINBOX_SESSION_SHARE/xrandr_screens_after_suspend.tgz ] && cat $PLAINBOX_SESSION_SHARE/xrandr_screens_after_suspend.tgz _description: This attaches screenshots from the suspend/cycle_resolutions_after_suspend_auto test to the results submission. plugin: shell id: suspend/record_playback_after_suspend estimated_duration: 10.0 depends: suspend/suspend_advanced requires: package.name == 'python3-gi' package.name == 'gir1.2-gstreamer-1.0' package.name == 'libgstreamer1.0-0' package.name == 'gstreamer1.0-plugins-good' package.name == 'gstreamer1.0-pulseaudio' package.name == 'alsa-base' device.category == 'AUDIO' command: audio_test _description: This will check to make sure that your audio device works properly after a suspend and resume. This may work fine with speakers and onboard microphone, however, it works best if used with a cable connecting the audio-out jack to the audio-in jack. plugin: shell id: suspend/record_playback_after_suspend_auto estimated_duration: 10.0 depends: suspend/suspend_advanced_auto requires: package.name == 'python3-gi' package.name == 'gir1.2-gstreamer-1.0' package.name == 'libgstreamer1.0-0' package.name == 'gstreamer1.0-plugins-good' package.name == 'gstreamer1.0-pulseaudio' package.name == 'alsa-base' device.category == 'AUDIO' command: audio_test _description: This will check to make sure that your audio device works properly after a suspend and resume. This may work fine with speakers and onboard microphone, however, it works best if used with a cable connecting the audio-out jack to the audio-in jack. plugin: attachment id: suspend/suspend-auto-single-log-attach command: [ -e $PLAINBOX_SESSION_SHARE/suspend_auto_single_log ] && cat $PLAINBOX_SESSION_SHARE/suspend_auto_single_log _description: Attaches the log from the single suspend/resume test to the results plugin: shell id: suspend/screenshot_after_suspend estimated_duration: 10.0 depends: suspend/suspend_advanced_auto requires: package.name == 'fswebcam' command: set -o pipefail; camera_test still --device=/dev/external_webcam -f ${PLAINBOX_SESSION_SHARE}/screenshot_after_suspend.jpg -q 2>&1 | ansi_parser _description: PURPOSE: Take a screengrab of the current screen after suspend (logged on Unity desktop) STEPS: 1. Take picture using USB webcam VERIFICATION: Review attachment manually later plugin: attachment id: screenshot_after_suspend.jpg depends: suspend/screenshot_after_suspend command: base64 ${PLAINBOX_SESSION_SHARE}/screenshot_after_suspend.jpg _description: Attaches the screenshot captured in graphics/screenshot. plugin: shell id: suspend/gpu_lockup_after_suspend estimated_duration: 10.0 requires: package.name == 'wmctrl' package.name == 'mesa-utils' package.name == 'firefox' depends: suspend/suspend_advanced_auto command: gpu_test _description: PURPOSE: Do some challenging operations then check for lockup on the GPU STEPS: 1. Create 2 glxgears windows and move them quickly 2. Switch workspaces with wmctrl 3. Launch an HTML5 video playback in firefox VERIFICATION: After a 60s workload, check kern.log for reported GPU errors plugin: shell id: suspend/wifi_resume_time depends: suspend/suspend_advanced requires: device.category == 'WIRELESS' command: network_reconnect_resume_test -t 90 -d wifi estimated_duration: 0.530 _description: Checks the length of time it takes to reconnect an existing wifi connection after a suspend/resume cycle. plugin: shell id: suspend/wifi_resume_time_auto estimated_duration: 1.2 depends: suspend/suspend_advanced_auto requires: device.category == 'WIRELESS' command: network_reconnect_resume_test -t 90 -d wifi _description: Checks the length of time it takes to reconnect an existing wifi connection after a suspend/resume cycle. plugin: shell id: suspend/network_resume_time depends: suspend/suspend_advanced estimated_duration: 1.2 requires: device.category == 'NETWORK' command: network_reconnect_resume_test -t 10 -d wired _description: Checks the length of time it takes to reconnect an existing wired connection after a suspend/resume cycle. plugin: shell id: suspend/network_resume_time_auto depends: suspend/suspend_advanced_auto estimated_duration: 1.2 requires: device.category == 'NETWORK' command: network_reconnect_resume_test -t 10 -d wired _description: Checks the length of time it takes to reconnect an existing wired connection after a suspend/resume cycle. plugin: manual id: suspend/led_after_suspend/power depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that the power LED operated the same after resuming from suspend STEPS: 1. Power LED should be on while device is switched on VERIFICATION: Does the power LED remain on after resuming from suspend? plugin: manual id: suspend/led_after_suspend/battery-charging estimated_duration: 120.0 depends: suspend/suspend_advanced _description: PURPOSE: Validate that the battery light shows charging status after resuming from suspend STEPS: 1. Let system run on battery for a while 2. Plug in AC plug VERIFICATION: Did the battery indicator LED still turn orange after resuming from suspend? plugin: manual id: suspend/led_after_suspend/battery-charged depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that the battery LED properly displays charged status after resuming from suspend STEPS: 1. Let system run on battery for a short time 2. Plug in AC 3. Let system run on AC VERIFICATION: Does the orange battery LED still shut off when system is fully charged after resuming from suspend? plugin: manual id: suspend/led_after_suspend/battery-low depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that the battery LED indicated low power after resuming from suspend STEPS: 1. Let system run on battery for several hours 2. Monitor battery LED carefully VERIFICATION: Does the LED light orange when battery is low after resuming from suspend? plugin: user-interact-verify id: suspend/led_after_suspend/hdd depends: suspend/suspend_advanced estimated_duration: 120.0 command: led_hdd_test _description: PURPOSE: Validate that the HDD LED still operates as expected after resuming from suspend STEPS: 1. Select "Test" to write and read a temporary file for a few seconds 2. HDD LED should blink when writing to/reading from HDD VERIFICATION: Did the HDD LED still blink with HDD activity after resuming from suspend? plugin: user-interact-verify id: suspend/led_after_suspend/numeric-keypad estimated_duration: 120.0 depends: suspend/suspend_advanced command: keyboard_test _description: PURPOSE: Validate that the numeric keypad LED operates the same before and after resuming from suspend STEPS: 1. Press "Block Num" key to toggle numeric keypad LED 2. Click on the "Test" button to open a window to verify your typing 3. Type using the numeric keypad both when the LED is on and off VERIFICATION: 1. Numeric keypad LED status should toggle everytime the "Block Num" key is pressed 2. Numbers should only be entered in the keyboard verification window when the LED is on plugin: manual id: suspend/led_after_suspend/caps-lock depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that the Caps Lock key operates the same before and after resuming from suspend STEPS: 1. Press "Block Cap Keys" to activate/deactivate cap keys blocking 2. Cap Keys LED should be switched on/off every time the key is pressed VERIFICATION: Did the Cap Keys LED light as expected after resuming from suspend? plugin: manual id: suspend/led_after_suspend/wlan depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: WLAN LED verification after resuming from suspend STEPS: 1. Make sure WLAN connection is established 2. WLAN LED should light VERIFICATION: Did the WLAN LED light as expected after resuming from suspend? plugin: manual id: suspend/led_after_suspend/wlan-disabled depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that WLAN LED shuts off when disabled after resuming from suspend STEPS: 1. Connect to AP 2. Use Physical switch to disable WLAN 3. Re-enable 4. Use Network-Manager to disable WLAN VERIFICATION: Did the LED turn off then WLAN is disabled after resuming from suspend? plugin: manual id: suspend/led_after_suspend/bluetooth depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate that the Bluetooth LED turns on and off when BT is enabled/disabled after resuming from suspend STEPS: 1. Switch bluetooth off from a hardware switch (if present) 2. Switch bluetooth back on 3. Switch bluetooth off from the panel applet 4. Switch bluetooth back on VERIFICATION: Did the bluetooth LED turn off and on twice after resuming from suspend? plugin: user-interact-verify id: suspend/led_after_suspend/camera estimated_duration: 120.0 depends: camera/detect suspend/suspend_advanced command: camera_test led _description: PURPOSE: Validate that the camera LED still works as expected after resuming from suspend STEPS: 1. Select Test to activate camera 2. Camera LED should light for a few seconds VERIFICATION: Did the camera LED still turn on and off after resuming from suspend? plugin: manual id: suspend/led_after_suspend/touchpad depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Touchpad LED verification after resuming from suspend STEPS: 1. Click on the touchpad button or press key combination to enable/disable touchpad button 2. Slide your finger on the touchpad VERIFICATION: 1. Touchpad LED status should toggle everytime the button is clicked or the key combination is pressed 2. When the LED is on, the mouse pointer should move on touchpad usage 3. When the LED is off, the mouse pointer should not move on touchpad usage plugin: manual id: suspend/led_after_suspend/wireless depends: suspend/suspend_advanced estimated_duration: 120.0 _description: PURPOSE: Validate Wireless (WLAN + Bluetooth) LED operated the same after resuming from suspend STEPS: 1. Make sure WLAN connection is established and Bluetooth is enabled. 2. WLAN/Bluetooth LED should light 3. Switch WLAN and Bluetooth off from a hardware switch (if present) 4. Switch them back on 5. Switch WLAN and Bluetooth off from the panel applet 6. Switch them back on VERIFICATION: Did the WLAN/Bluetooth LED light as expected after resuming from suspend? plugin: manual id: suspend/keys_after_suspend/brightness depends: suspend/suspend_advanced estimated_duration: 120.0 requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will test the brightness key after resuming from suspend STEPS: 1. Press the brightness buttons on the keyboard VERIFICATION: Did the brightness change following to your key presses after resuming from suspend? plugin: user-interact-verify id: suspend/keys_after_suspend/volume depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe02e,0xe0ae:Volume Up' '0xe030,0xe0b0:Volume Down' _description: PURPOSE: This test will test the volume keys of your keyboard after resuming from suspend STEPS: Skip this test if your computer has no volume keys. 1. Click test to open a window on which to test the volume keys. 2. If all the keys work, the test will be marked as passed. VERIFICATION: Did the volume change following to your key presses? plugin: user-interact-verify id: suspend/keys_after_suspend/mute depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe020,0xe0a0:Mute' _description: PURPOSE: This test will test the mute key of your keyboard after resuming from suspend STEPS: 1. Click test to open a window on which to test the mute key. 2. If the key works, the test will pass and the window will close. VERIFICATION: Did the volume mute following your key presses? plugin: manual id: suspend/keys_after_suspend/sleep depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'KEYBOARD' _description: PURPOSE: This test will test the sleep key after resuming from suspend STEPS: 1. Press the sleep key on the keyboard 2. Wake your system up by pressing the power button VERIFICATION: Did the system go to sleep after pressing the sleep key after resuming from suspend? plugin: user-interact-verify id: suspend/keys_after_suspend/battery-info depends: suspend/suspend_advanced estimated_duration: 120.0 requires: dmi.product in ['Notebook','Laptop','Portable'] user: root command: key_test -s '0xe071,0xef1:Battery Info' _description: PURPOSE: This test will test the battery information key after resuming from suspend STEPS: Skip this test if you do not have a Battery Button. 1. Click Test to begin 2. Press the Battery Info button (or combo like Fn+F3) 3: Close the Power Statistics tool if it opens VERIFICATION: Did the Battery Info key work as expected after resuming from suspend? plugin: manual id: suspend/keys_after_suspend/wireless depends: suspend/suspend_advanced estimated_duration: 120.0 requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: This test will test the wireless key after resuming from suspend STEPS: 1. Press the wireless key on the keyboard 2. Press the same key again VERIFICATION: Did the wireless go off on the first press and on again on the second after resuming from suspend? plugin: user-interact-verify id: suspend/keys_after_suspend/media-control estimated_duration: 120.0 depends: suspend/suspend_advanced requires: device.category == 'KEYBOARD' user: root command: key_test -s 0xe010,0xe090:Previous 0xe024,0xe0a4:Stop 0xe019,0xe099:Next 0xe022,0xe0a2:Play _description: PURPOSE: This test will test the media keys of your keyboard after resuming from suspend STEPS: Skip this test if your computer has no media keys. 1. Click test to open a window on which to test the media keys. 2. If all the keys work, the test will be marked as passed. VERIFICATION: Do the keys work as expected after resuming from suspend? plugin: user-interact-verify id: suspend/keys_after_suspend/super depends: suspend/suspend_advanced estimated_duration: 120.0 requires: device.category == 'KEYBOARD' user: root command: key_test -s '0xe05b,0xe0db:Left Super Key' _description: PURPOSE: This test will test the super key of your keyboard after resuming from suspend STEPS: 1. Click test to open a window on which to test the super key. 2. If the key works, the test will pass and the window will close. VERIFICATION: Does the super key work as expected after resuming from suspend? plugin: manual id: suspend/keys_after_suspend/video-out depends: suspend/suspend_advanced estimated_duration: 120.0 requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: Validate that the External Video hot key is working as expected after resuming from suspend STEPS: 1. Plug in an external monitor 2. Press the display hot key to change the monitors configuration VERIFICATION: Check that the video signal can be mirrored, extended, displayed on external or onboard only, after resuming from suspend. plugin: manual id: suspend/keys_after_suspend/touchpad depends: suspend/suspend_advanced estimated_duration: 120.0 requires: dmi.product in ['Notebook','Laptop','Portable'] _description: PURPOSE: Verify touchpad hotkey toggles touchpad functionality on and off after resuming from suspend STEPS: 1. Verify the touchpad is functional 2. Tap the touchpad toggle hotkey 3. Tap the touchpad toggle hotkey again VERIFICATION: Verify the touchpad has been disabled and re-enabled. plugin: user-interact id: suspend/usb_insert_after_suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher insert usb _description: PURPOSE: This test will check that the system correctly detects the insertion of a USB storage device after suspend and resume. STEPS: 1. Click "Test" and insert a USB storage device (pen-drive/HDD). (Note: this test will time-out after 20 seconds.) 2. Do not unplug the device after the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: suspend/usb3_insert_after_suspend estimated_duration: 30.0 requires: usb.usb3 == 'supported' depends: suspend/suspend_advanced command: removable_storage_watcher -m 500000000 insert usb _description: PURPOSE: This test will check that the system correctly detects the insertion of a USB 3.0 storage device after suspend and resume. STEPS: 1. Click "Test" and insert a USB 3.0 storage device (pen-drive/HDD) in a USB 3.0 port. (Note: this test will time-out after 20 seconds.) 2. Do not unplug the device after the test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: suspend/usb_remove_after_suspend estimated_duration: 30.0 depends: suspend/usb_insert_after_suspend command: removable_storage_watcher remove usb _description: PURPOSE: This test will check that the system correctly detects the removal of a USB storage device after suspend. STEPS: 1. Click "Test" and remove the USB device. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: suspend/usb3_remove_after_suspend estimated_duration: 30.0 depends: suspend/usb3_insert_after_suspend requires: usb.usb3 == 'supported' command: removable_storage_watcher -m 500000000 remove usb _description: PURPOSE: This test will check that the system correctly detects the removal of a USB 3.0 storage device after suspend STEPS: 1. Click "Test" and remove the USB 3.0 device. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: suspend/usb_storage_automated_after_suspend estimated_duration: 1.2 depends: suspend/usb_insert_after_suspend user: root command: removable_storage_test -s 268400000 usb _description: This test is automated and executes after the suspend/usb_insert_after_suspend test is run. plugin: shell id: suspend/usb3_storage_automated_after_suspend estimated_duration: 1.2 requires: usb.usb3 == 'supported' depends: suspend/usb3_insert_after_suspend user: root command: removable_storage_test -s 268400000 -m 500000000 -p 7 usb _description: This test is automated and executes after the suspend/usb3_insert_after_suspend test is run. plugin: shell id: suspend/usb_storage_preinserted_after_suspend estimated_duration: 1.2 user: root depends: suspend/suspend_advanced_auto command: removable_storage_test -l usb && removable_storage_test -s 268400000 usb _description: This is an automated version of usb/storage-automated and assumes that the server has usb storage devices plugged in prior to checkbox execution. It is intended for servers and SRU automated testing. plugin: shell id: suspend/usb3_storage_preinserted_after_suspend estimated_duration: 1.2 user: root requires: usb.usb3 == 'supported' depends: suspend/suspend_advanced_auto command: removable_storage_test -l usb && removable_storage_test -s 268400000 -m 500000000 -p 7 usb _description: This is an automated version of usb3/storage-automated and assumes that the server has usb 3.0 storage devices plugged in prior to checkbox execution. It is intended for servers and SRU automated testing. plugin: shell id: suspend/usb_performance_after_suspend depends: suspend/usb_insert_after_suspend user: root estimated_duration: 45.00 command: removable_storage_test -s 268400000 -p 15 usb _description: This test will check that your USB 2.0 port transfers data at a minimum expected speed. plugin: shell id: suspend/usb3_superspeed_performance_after_suspend requires: usb.usb3 == 'supported' depends: suspend/usb3_insert_after_suspend user: root estimated_duration: 45.00 command: removable_storage_test -s 268400000 -m 500000000 -p 60 usb _description: This test will check that your USB 3.0 port transfers data at a minimum expected speed in accordance with the specification of USB 3.0 SuperSpeed mode. plugin: user-interact id: suspend/mmc-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of an MMC card after the system has been suspended STEPS: 1. Click "Test" and insert an MMC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: suspend/mmc-storage-after-suspend depends: suspend/mmc-insert-after-suspend estimated_duration: 10.0 user: root command: removable_storage_test -s 67120000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/mmc-insert-after-suspend test is run. It tests reading and writing to the MMC card after the system has been suspended. plugin: user-interact id: suspend/mmc-remove-after-suspend depends: suspend/mmc-insert-after-suspend estimated_duration: 30.0 command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of an MMC card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the MMC card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: suspend/sd-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of an UNLOCKED SD card after the system has been suspended STEPS: 1. Click "Test" and insert an UNLOCKED SD card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: suspend/sd-storage-after-suspend estimated_duration: 10.0 depends: suspend/sd-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sd-insert-after-suspend test is run. It tests reading and writing to the SD card after the system has been suspended. plugin: user-interact id: suspend/sd-remove-after-suspend estimated_duration: 30.0 depends: suspend/sd-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of an SD card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the SD card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: suspend/sdhc-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of an UNLOCKED SDHC media card after the system has been suspended STEPS: 1. Click "Test" and insert an UNLOCKED SDHC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: suspend/sdhc-storage-after-suspend estimated_duration: 10.0 depends: suspend/sdhc-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sdhc-insert-after-suspend test is run. It tests reading and writing to the SDHC card after the system has been suspended. plugin: user-interact id: suspend/sdhc-remove-after-suspend estimated_duration: 30.0 depends: suspend/sdhc-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of an SDHC card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the SDHC card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/cf-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a CF card after the system has been suspended STEPS: 1. Click "Test" and insert a CF card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/cf-storage-after-suspend estimated_duration: 10.0 depends: mediacard/cf-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/cf-insert-after-suspend test is run. It tests reading and writing to the CF card after the system has been suspended. plugin: user-interact id: mediacard/cf-remove-after-suspend estimated_duration: 30.0 depends: mediacard/cf-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a CF card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the CF card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/sdxc-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a SDXC card after the system has been suspended STEPS: 1. Click "Test" and insert a SDXC card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/sdxc-storage-after-suspend estimated_duration: 10.0 depends: mediacard/sdxc-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/sdxc-insert-after-suspend test is run. It tests reading and writing to the SDXC card after the system has been suspended. plugin: user-interact id: mediacard/sdxc-remove-after-suspend depends: mediacard/sdxc-insert-after-suspend estimated_duration: 30.0 command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a SDXC card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the SDXC card from the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/ms-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a MS card after the system has been suspended STEPS: 1. Click "Test" and insert a MS card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/ms-storage-after-suspend estimated_duration: 10.0 depends: mediacard/ms-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/ms-insert-after-suspend test is run. It tests reading and writing to the MS card after the system has been suspended. plugin: user-interact id: mediacard/ms-remove-after-suspend estimated_duration: 30.0 depends: mediacard/ms-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a MS card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the MS card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/msp-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a MSP card after the system has been suspended STEPS: 1. Click "Test" and insert a MSP card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/msp-storage-after-suspend estimated_duration: 10.0 depends: mediacard/msp-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/msp-insert-after-suspend test is run. It tests reading and writing to the MSP card after the system has been suspended. plugin: user-interact id: mediacard/msp-remove-after-suspend estimated_duration: 30.0 depends: mediacard/msp-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a MSP card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the MSP card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: user-interact id: mediacard/xd-insert-after-suspend estimated_duration: 30.0 depends: suspend/suspend_advanced command: removable_storage_watcher --memorycard insert sdio usb scsi _description: PURPOSE: This test will check that the systems media card reader can detect the insertion of a xD card after the system has been suspended STEPS: 1. Click "Test" and insert a xD card into the reader. If a file browser opens up, you can safely close it. (Note: this test will time-out after 20 seconds.) 2. Do not remove the device after this test. VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: mediacard/xd-storage-after-suspend estimated_duration: 10.0 depends: mediacard/xd-insert-after-suspend user: root command: removable_storage_test -s 268400000 --memorycard sdio usb scsi _description: This test is automated and executes after the mediacard/xd-insert-after-suspend test is run. It tests reading and writing to the xD card after the system has been suspended. plugin: user-interact id: mediacard/xd-remove-after-suspend estimated_duration: 30.0 depends: mediacard/xd-insert-after-suspend command: removable_storage_watcher --memorycard remove sdio usb scsi _description: PURPOSE: This test will check that the system correctly detects the removal of a xD card from the systems card reader after the system has been suspended. STEPS: 1. Click "Test" and remove the xD card from the reader. (Note: this test will time-out after 20 seconds.) VERIFICATION: The verification of this test is automated. Do not change the automatically selected result. plugin: shell id: touchpad/touchpad_after_suspend depends: suspend/suspend_advanced_auto requires: dmi.product in ['Notebook','Laptop','Portable'] xinput.device_class == 'XITouchClass' and xinput.touch_mode != 'dependent' command: true estimated_duration: 1.2 _description: Determine if the touchpad is still functioning after suspend/resume. plugin: manual id: touchscreen/touchscreen_after_suspend depends: suspend/suspend_advanced_auto estimated_duration: 30.0 _description: PURPOSE: Check touchscreen tap recognition STEPS: 1. Tap an object on the screen with finger. The cursor should jump to location tapped and object should highlight VERIFICATION: Does tap recognition work? 2013.com.canonical.certification.checkbox-0.4/jobs/virtualization.txt.in0000664000175000017500000000135012320565736026223 0ustar zygazyga00000000000000plugin: shell id: virtualization/kvm_check_vm user: root environ: http_proxy https_proxy requires: package.name == 'qemu-kvm' package.name == 'qemu-utils' virtualization.kvm == 'supported' command: virtualization kvm --debug _description: Test to check that a cloud image boots and works properly with KVM plugin: shell id: virtualization/xen_ok requires: package.name == 'libvirt-bin' user: root command: virsh -c xen:/// domstate Domain-0 _description: Test to verify that the Xen Hypervisor is running. plugin: shell id: virtualization/xen_check_vm depends: virtualization/xen_ok user: root command: xen_test /images/xentest.img /images/xentest.xml _description: Test to check that a Xen domU image can boot and run on Xen on Ubuntu 2013.com.canonical.certification.checkbox-0.4/jobs/rendercheck.txt.in0000664000175000017500000000142712320567463025420 0ustar zygazyga00000000000000plugin: shell id: rendercheck/tests requires: package.name == 'x11-apps' command: ( rendercheck_test -b repeat -b gradients -d -o $PLAINBOX_SESSION_SHARE/rendercheck-results && echo "Rendercheck tests completed successfully" ) || ( echo "Error running rendercheck. Please see the log $PLAINBOX_SESSION_SHARE/rendercheck-results for details" >&2 && false ) _description: Runs all of the rendercheck test suites. This test can take a few minutes. plugin: attachment id: rendercheck/tarball depends: rendercheck/tests command: [ -e $PLAINBOX_SESSION_SHARE/rendercheck-results ] && tar cvfz $PLAINBOX_SESSION_SHARE/rendercheck-results.tar.gz $PLAINBOX_SESSION_SHARE/rendercheck-results && cat $PLAINBOX_SESSION_SHARE/rendercheck-results.tar.gz _description: Attach log from rendercheck tests 2013.com.canonical.certification.checkbox-0.4/jobs/local.txt.in0000664000175000017500000001552212320567463024236 0ustar zygazyga00000000000000id: __audio__ plugin: local _description: Audio tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/audio.txt?(.in) id: __benchmarks__ plugin: local _description: Benchmarks tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/benchmarks.txt?(.in) id: __bluetooth__ plugin: local _description: Bluetooth tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/bluetooth.txt?(.in) id: __camera__ plugin: local _description: Camera tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/camera.txt?(.in) id: __codecs__ plugin: local _description: Codec tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/codecs.txt?(.in) id: __cpu__ plugin: local _description: CPU tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/cpu.txt?(.in) id: __daemons__ plugin: local _description: System Daemon tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/daemons.txt?(.in) id: __disk__ plugin: local _description: Disk tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/disk.txt?(.in) id: __ethernet__ plugin: local _description: Ethernet Device tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/ethernet.txt?(.in) id: __esata__ plugin: local _description: eSATA disk tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/esata.txt?(.in) id: __fingerprint__ plugin: local _description: Fingerprint reader tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/fingerprint.txt?(.in) id: __firewire__ plugin: local _description: Firewire disk tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/firewire.txt?(.in) id: __firmware__ plugin: local _description: Firmware tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/firmware.txt?(.in) id: __floppy__ plugin: local _description: Floppy disk tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/floppy.txt?(.in) id: __graphics__ plugin: local _description: Graphics tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/graphics.txt?(.in) id: __hibernate__ plugin: local _description: Hibernation tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/hibernate.txt?(.in) id: __info__ plugin: local _description: Informational tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/info.txt?(.in) id: __input__ plugin: local _description: Input Devices tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/input.txt?(.in) id: __install__ plugin: local _description: Software Installation tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/install.txt?(.in) id: __keys__ plugin: local _description: Hotkey tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/keys.txt?(.in) id: __led__ plugin: local _description: LED tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/led.txt?(.in) id: __mediacard__ plugin: local _description: Media Card tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/mediacard.txt?(.in) id: __memory__ plugin: local _description: Memory tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/memory.txt?(.in) id: __rendercheck__ plugin: local _description: Rendercheck tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/rendercheck.txt?(.in) id: __mir__ plugin: local _description: MIR tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/mir.txt?(.in) id: __miscellanea__ plugin: local _description: Miscellaneous tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/miscellanea.txt?(.in) id: __monitor__ plugin: local _description: Monitor tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/monitor.txt?(.in) id: __networking__ plugin: local _description: Non-device specific networking tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/networking.txt?(.in) id: __optical__ plugin: local _description: Optical Drive tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/optical.txt?(.in) id: __panel_clock__ plugin: local _description: Panel Clock Verification tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/panel_clock_test.txt?(.in) id: __panel_reboot__ plugin: local _description: Panel Reboot Verification tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/panel_reboot.txt?(.in) id: __expresscard__ plugin: local _description: ExpressCard tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/expresscard.txt?(.in) id: __peripheral__ plugin: local _description: Peripheral tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/peripheral.txt?(.in) id: __piglit__ plugin: local _description: Piglit tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/piglit.txt?(.in) id: __power-management__ plugin: local _description: Power Management tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/power-management.txt?(.in) id: __server-services__ plugin: local _description: Server Services checks command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/server-services.txt?(.in) id: __suspend__ plugin: local _description: Suspend tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/suspend.txt?(.in) id: __touchpad__ plugin: local _description: Touchpad tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/touchpad.txt?(.in) id: __touchscreen__ plugin: local _description: Touchscreen tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/touchscreen.txt?(.in) id: __usb__ plugin: local _description: USB tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/usb.txt?(.in) id: __user_apps__ plugin: local _description: User Applications command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/user_apps.txt?(.in) id: __virtualization__ plugin: local _description: Virtualization tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/virtualization.txt?(.in) id: __wireless__ plugin: local _description: Wireless networking tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/wireless.txt?(.in) id: __mobilebroadband__ plugin: local _description: Mobile broadband tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/mobilebroadband.txt?(.in) id: __stress__ plugin: local _description: Stress tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/stress.txt?(.in) id: __smoke__ plugin: local _description: Smoke tests command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/smoke.txt?(.in) id: __sniff__ plugin: local _description: Sniff Sniffers command: shopt -s extglob cat $PLAINBOX_PROVIDER_DATA/../jobs/sniff.txt?(.in) 2013.com.canonical.certification.checkbox-0.4/jobs/daemons.txt.in0000664000175000017500000000412212320565736024565 0ustar zygazyga00000000000000plugin: shell id: daemons/atd requires: package.name == 'at' command: pgrep -f '/usr/sbin/atd' >/dev/null _description: Test if the atd daemon is running when the package is installed. plugin: shell id: daemons/cron requires: package.name == 'cron' command: pgrep -f '/usr/sbin/cron' >/dev/null _description: Test if the cron daemon is running when the package is installed. plugin: shell id: daemons/cupsd requires: package.name == 'cupsys' command: pgrep -f '/usr/sbin/cupsd' >/dev/null _description: Test if the cupsd daemon is running when the package is installed. plugin: shell id: daemons/getty requires: package.name == 'util-linux' command: pgrep -f '/sbin/getty' >/dev/null _description: Test if the getty daemon is running when the package is installed. plugin: shell id: daemons/init requires: package.name == 'upstart' command: pgrep -f '/sbin/init' >/dev/null _description: Test if the init daemon is running when the package is installed. plugin: shell id: daemons/klogd requires: package.name == 'klogd' command: pgrep -f '/sbin/klogd' >/dev/null _description: Test if the klogd daemon is running when the package is installed. plugin: shell id: daemons/nmbd requires: package.name == 'samba' command: pgrep -f '/usr/sbin/nmbd' >/dev/null _description: Test if the nmbd daemon is running when the package is installed. plugin: shell id: daemons/smbd requires: package.name == 'samba' command: pgrep -f '/usr/sbin/smbd' >/dev/null _description: Test if the smbd daemon is running when the package is installed. plugin: shell id: daemons/syslogd requires: package.name == 'syslogd' command: pgrep -f '/sbin/syslogd' >/dev/null _description: Test if the syslogd daemon is running when the package is installed. plugin: shell id: daemons/udevd requires: package.name == 'udevd' command: pgrep -f '/sbin/udevd' >/dev/null _description: Test if the udevd daemon is running when the package is installed. plugin: shell id: daemons/winbindd requires: package.name == 'winbind' command: pgrep -f '/usr/sbin/winbindd' >/dev/null _description: Test if the winbindd daemon is running when the package is installed. 2013.com.canonical.certification.checkbox-0.4/jobs/optical.txt.in0000664000175000017500000002026612320565736024601 0ustar zygazyga00000000000000plugin: shell id: optical/detect requires: device.category == 'CDROM' estimated_duration: 1.2 _description: Test to detect the optical drives command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' | sed '/^$/d' $vendor $product EOF plugin: local id: optical/read requires: device.category == 'CDROM' _description: Optical read test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: user-interact-verify id: optical/read_`ls /sys$path/block` requires: device.path == "$path" estimated_duration: 120.0 user: root command: optical_read_test /dev/`ls /sys$path/block` description: PURPOSE: This test will check your $product device's ability to read CD media STEPS: 1. Insert appropriate non-blank media into your optical drive(s). Movie and Audio Disks may not work. Self-created data disks have the greatest chance of working. 2. If a file browser window opens, you can safely close or ignore that window. 3. Click "Test" to begin the test. VERIFICATION: This test should automatically select "Yes" if it passes, "No" if it fails. EOF plugin: local id: optical/read-automated requires: device.category == 'CDROM' _description: Automated optical read test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: shell id: optical/read-automated_`ls /sys$path/block` estimated_duration: 120.0 requires: device.path == "$path" user: root command: optical_read_test /dev/`ls /sys$path/block` description: This is an automated version of optical/read. It assumes you have already inserted a data CD into your optical drive prior to running Checkbox. EOF plugin: local id: optical/cdrom-write requires: device.category == 'CDROM' _description: CD write test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: user-interact-verify id: optical/cdrom-write_`ls /sys$path/block` estimated_duration: 120.0 requires: device.path == "$path" optical_drive_`ls /sys$path/block`.cd_write == 'supported' user: root command: set -o pipefail; optical_write_test /dev/`ls /sys$path/block` cd | ansi_parser description: PURPOSE: This test will check your system's $product CD writing capabilities. This test requires a blank CD-R or CD+R. STEPS: Skip this test if you do not have a blank CD disk. 1. Insert a blank CD-R or CD+R into your drive 2. Click "Test" to begin. 3. When the CD tray ejects the media after burning, close it (DO NOT remove the disk, it is needed for the second portion of the test). Note, you must close the drive within 5 minutes or the test will time out. VERIFICATION: This test should automatically select "Yes" if it passes, "No" if it fails. EOF plugin: local id: optical/cdrom-write-automated requires: device.category == 'CDROM' _description: Automated CD write test command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: shell id: optical/cdrom-write-automated_`ls /sys$path/block` estimated_duration: 120.0 requires: device.path == "$path" optical_drive_`ls /sys$path/block`.cd_write == 'supported' user: root command: set -o pipefail; optical_write_test /dev/`ls /sys$path/block` cd | ansi_parser description: This is an automated version of optical/cdrom-write. It assumes you have already inserted a data CD into your optical drive prior to running Checkbox. EOF plugin: manual id: optical/cdrom-audio-playback depends: optical/read estimated_duration: 120.0 _description: PURPOSE: This test will check your CD audio playback capabilities STEPS: 1. Insert an audio CD in your optical drive 2. When prompted, launch the Music Player 3. Locate the CD in the display of the Music Player 4. Select the CD in the Music Player 5. Click the Play button to listen to the music on the CD 6. Stop playing after some time 7. Right click on the CD icon and select "Eject Disc" 8. The CD should be ejected 9. Close the Music Player VERIFICATION: Did all the steps work? plugin: local id: optical/dvd-write requires: device.category == 'CDROM' _description: DVD write test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: user-interact-verify id: optical/dvd-write_`ls /sys$path/block` requires: device.path == "$path" optical_drive_`ls /sys$path/block`.dvd_write == 'supported' estimated_duration: 120.0 user: root command: set -o pipefail; optical_write_test /dev/`ls /sys$path/block` dvd | ansi_parser description: PURPOSE: This test will check your system's $product writing capabilities. This test requires a blank DVD-R or DVD+R. STEPS: Skip this test if you do not have a blank DVD disk. 1. Enter a blank DVD-R or DVD+R into your drive 2. Click "Test" to begin. 3. When the CD tray ejects the media after burning, close it (DO NOT remove the disk, it is needed for the second portion of the test). Note, you must close the drive within 5 minutes or the test will time out. VERIFICATION: This test should automatically select "Yes" if it passes, "No" if it fails. EOF plugin: local id: optical/dvd-write-automated requires: device.category == 'CDROM' _description: Automated DVD write test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: shell id: optical/dvd-write-automated_`ls /sys$path/block` estimated_duration: 120.0 requires: device.path == "$path" optical_drive_`ls /sys$path/block`.dvd_write == 'supported' user: root command: set -o pipefail; optical_write_test /dev/`ls /sys$path/block` dvd | ansi_parser description: This is an automated version of optical/dvd-write. It assumes you have already inserted a data DVD into your optical drive prior to running Checkbox. EOF plugin: user-interact-verify id: optical/dvd_playback command: totem /media/cdrom estimated_duration: 120.0 requires: device.category == 'CDROM' package.name == 'totem' _description: PURPOSE: This test will check your DVD playback capabilities STEPS: 1. Insert a DVD that contains any movie in your optical drive 2. Click "Test" to play the DVD in Totem VERIFICATION: Did the file play? plugin: local id: optical/bluray-read requires: device.category == 'CDROM' _description: Automated Blu-Ray read test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: user-interact id: optical/bluray-read_`ls /sys$path/block` estimated_duration: 120.0 requires: device.path == "$path" optical_drive_`ls /sys$path/block`.bd_read == "supported" user: root command: optical_read_test /dev/`ls /sys$path/block` estimated_duration: 10.00 description: PURPOSE: This test will check your $product device's ability to read Blu-Ray (BD) media STEPS: 1. Insert appropriate non-blank media into your Blu-Ray drive. Movie and Audio Disks may not work. Self-created data disks have the greatest chance of working. 2. If a file browser window opens, you can safely close or ignore that window. 3. Click "Test" to begin the test. VERIFICATION: This test should automatically select "Yes" if it passes, "No" if it fails. EOF plugin: local id: optical/bluray-write requires: device.category == 'CDROM' _description: Automated Blu-Ray write test. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=CDROM"' plugin: user-interact id: optical/bluray-write_`ls /sys$path/block` requires: device.path == "$path" optical_drive_`ls /sys$path/block`.bd_write == "supported" package.name == "growisofs" user: root command: set -o pipefail; optical_write_test /dev/`ls /sys$path/block` bd | ansi_parser estimated_duration: 120.00 description: PURPOSE: This test will check your $product device's ability to write Blu-Ray (BD) media STEPS: Skip this test if you do not have a blank BD-R disc 1. Insert appropriate writable media into your Blu-Ray drive. 2. Click "Test" to begin the test. VERIFICATION: This test should automatically select "Yes" if it passes, "No" if it fails. EOF 2013.com.canonical.certification.checkbox-0.4/jobs/camera.txt.in0000664000175000017500000000314112320565736024367 0ustar zygazyga00000000000000plugin: shell id: camera/detect estimated_duration: 1.2 requires: device.category == 'CAPTURE' command: camera_test detect _description: This Automated test attempts to detect a camera. plugin: user-verify id: camera/display estimated_duration: 120.0 depends: camera/detect requires: device.category == 'CAPTURE' command: camera_test display _description: PURPOSE: This test will check that the built-in camera works STEPS: 1. Click on Test to display a video capture from the camera for ten seconds. VERIFICATION: Did you see the video capture? plugin: user-verify id: camera/still estimated_duration: 120.0 depends: camera/detect requires: package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' package.name == 'eog' package.name == 'fswebcam' or package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' device.category == 'CAPTURE' command: camera_test still _description: PURPOSE: This test will check that the built-in camera works STEPS: 1. Click on Test to display a still image from the camera for ten seconds. VERIFICATION: Did you see the image? plugin: shell id: camera/multiple-resolution-images estimated_duration: 1.2 depends: camera/detect requires: package.name == 'fswebcam' or package.name == 'gir1.2-gst-plugins-base-0.10' or package.name == 'gir1.2-gst-plugins-base-1.0' device.category == 'CAPTURE' command: camera_test resolutions _description: Takes multiple pictures based on the resolutions supported by the camera and validates their size and that they are of a valid format. 2013.com.canonical.certification.checkbox-0.4/jobs/user_apps.txt.in0000664000175000017500000003636212320567463025152 0ustar zygazyga00000000000000plugin: user-interact-verify id: software/update_manager_finds_updates depends: ethernet/detect user: root requires: package.name == 'update-manager' _description: PURPOSE: This test will check that the update manager can find updates. STEPS: 1. Click Test to launch update-manager. 2. Follow the prompts and if updates are found, install them. 3. When Update Manager has finished, please close the app by clicking the Close button in the lower right corner. VERIFICATION: Did Update manager find and install updates (Pass if no updates are found, but Fail if updates are found but not installed) command: update-manager --check-dist-upgrades plugin: user-interact-verify id: software/nautilus_folder_create requires: package.name == 'nautilus' command: nautilus _description: PURPOSE: This test will check that the file browser can create a new folder. STEPS: 1. Click Test to open the File Browser. 2. On the menu bar, click File -> Create Folder. 3. In the name box for the new folder, enter the name Test Folder and hit Enter. 4. Close the File browser. VERIFICATION: Do you now have a new folder called Test Folder? plugin: user-interact-verify id: software/nautilus_folder_copy depends: software/nautilus_folder_create command: nautilus _description: PURPOSE: This test will check that the file browser can copy a folder STEPS: 1. Click Test to open the File Browser. 2. Right click on the folder called Test Folder and click on Copy. 3. Right Click on any white area in the window and click on Paste. 4. Right click on the folder called Test Folder(copy) and click Rename. 5. Enter the name Test Data in the name box and hit Enter. 6. Close the File browser. VERIFICATION: Do you now have a folder called Test Data? plugin: user-interact-verify id: software/nautilus_folder_move depends: software/nautilus_folder_copy command: nautilus _description: PURPOSE: This test will verify that the file browser can move a folder. STEPS: 1. Click Test to open the File Browser. 2. Click and drag the folder called Test Data onto the icon called Test Folder. 3. Release the button. 4. Double click the folder called Test Folder to open it up. 5. Close the File Browser. VERIFICATION: Was the folder called Test Data successfully moved into the folder called Test Folder? plugin: user-interact-verify id: software/nautilus_file_create depends: software/nautilus_folder_create command: nautilus $HOME/"Test Folder" _description: PURPOSE: This test will check that the file browser can create a new file. STEPS: 1. Click Select Test to open the File Browser. 2. Right click in the white space and click Create Document -> Empty Document. 3. Enter the name Test File 1 in the name box and hit Enter. 4. Close the File browser. VERIFICATION: Do you now have a file called Test File 1? plugin: user-interact-verify id: software/nautilus_file_copy depends: software/nautilus_file_create command: nautilus $HOME/"Test Folder" _description: PURPOSE: This test will check that the file browser can copy a file. STEPS: 1. Click Test to open the File Browser. 2. Right click on the file called Test File 1 and click Copy. 3. Right click in the white space and click Paste. 4. Right click on the file called Test File 1(copy) and click Rename. 5. Enter the name Test File 2 in the name box and hit Enter. 6. Close the File Browser. VERIFICATION: Do you now have a file called Test File 2? plugin: user-interact-verify id: software/nautilus_file_move depends: software/nautilus_file_copy command: nautilus $HOME/"Test Folder" _description: PURPOSE: This test will check that the file browser can move a file. STEPS: 1. Click Test to open the File Browser. 2. Click and drag the file called Test File 2 onto the icon for the folder called Test Data. 3. Release the button. 4. Double click the icon for Test Data to open that folder up. 5. Close the File Browser. VERIFICATION: Was the file Test File 2 successfully moved into the Test Data folder? plugin: user-interact-verify id: software/nautilus_file_delete depends: software/nautilus_file_create command: nautilus $HOME/"Test Folder" _description: PURPOSE: This test will check that the file browser can delete a file. STEPS: 1. Click Test to open the File Browser. 2. Right click on the file called Test File 1 and click on Move To Trash. 3. Verify that Test File 1 has been removed. 4. Close the File Browser. VERIFICATION: Is Test File 1 now gone? plugin: user-interact-verify id: software/nautilus_folder_delete depends: software/nautilus_folder_create command: nautilus _description: PURPOSE: This test will check that the file browser can delete a folder. STEPS: 1. Click Test to open the File Browser. 2. Right click on the folder called Test Folder and click on Move To Trash. 3. Verify that the folder was deleted. 4. Close the file browser. VERIFICATION: Has Test Folder been successfully deleted? plugin: local id: software/view_office_document requires: package.name == "ubuntu-desktop" _description: Common Document Types Test command: cat <<'EOF' | run_templates -s "find $PLAINBOX_PROVIDER_DATA/documents -type f" id: `basename $0`_test plugin: user-interact-verify description: PURPOSE: This test will check that common office document types can be opened in the default application. STEPS: 1. Click Test to open the file '$0' with its default viewer. 2. After the viewer opens, check out the file that was opened. 3. Close the application (LibreOffice, Doc Viewer, etc.) VERIFICATION: Did the application open the document properly? (e.g was it displayed and did it function properly?) command: xdg-open $0 EOF plugin: local id: software/audio_file_play requires: package.name == "ubuntu-desktop" _description: Common Document Types Test command: cat <<'EOF' | run_templates -s "find $PLAINBOX_PROVIDER_DATA/audio -type f" id: `basename $0`_test plugin: user-interact-verify description: PURPOSE: This test will check that common audio files can be opened in the default player. STEPS: 1. Click Test to open the audio file '$0' with its default player. 2. After the player opens listen to the sound. 3. Close the application. VERIFICATION: Did the application play the audio file properly? (no skips, crackles, etc) command: xdg-open $0 EOF plugin: local id: software/video_file_play requires: package.name == "ubuntu-desktop" _description: Common Document Types Test command: cat <<'EOF' | run_templates -s "find $PLAINBOX_PROVIDER_DATA/video -type f" id: `basename $0`_test plugin: user-interact-verify description: PURPOSE: This test will check that common video files can be played in the default player. STEPS: 1. Click Test to play the video '$0' with its default player. 2. Watch the video. 3. Close the application. VERIFICATION: Did the application play '$0' proplerly (no video or audio issues with playback?) command: xdg-open $0 EOF plugin: local id: software/view_image_file requires: package.name == "ubuntu-desktop" _description: Common Document Types Test command: cat <<'EOF' | run_templates -s "find $PLAINBOX_PROVIDER_DATA/images -type f" id: `basename $0`_test plugin: user-interact-verify description: PURPOSE: This test will check that common image formats can be opened in the default viewer. STEPS: 1. Click Test to attempt to open the image '$0' with its default viewer. 2. Check out the file that was opened. 3. Close the application. VERIFICATION: Did the image display properly? command: xdg-open $0 EOF plugin: user-interact-verify id: software/firefox requires: package.name == 'firefox' command: firefox $PLAINBOX_PROVIDER_DATA/websites/testindex.html _description: PURPOSE: This test will check that Firefox can render a basic web page. STEPS: 1. Select Test to launch Firefox and view the test web page. VERIFICATION: Did the Ubuntu Test page load correctly? plugin: user-interact-verify id: software/firefox-flash depends: software/firefox requires: package.name == 'firefox' command: firefox $PLAINBOX_PROVIDER_DATA/websites/flashtest.html _description: PURPOSE: This test will check that Firefox can run flash applications. Note: this may require installing additional software to successfully complete. STEPS: 1. Select Test to launch Firefox and view a sample Flash test. VERIFICATION: Did you see the text? plugin: user-interact-verify id: software/firefox-flash-video depends: software/firefox-flash requires: package.name == 'firefox' command: firefox $PLAINBOX_PROVIDER_DATA/websites/flashvideo.html _description: PURPOSE: This test will check that Firefox can play a Flash video. Note: this may require installing additional software to successfully complete. STEPS: 1. Select Test to launch Firefox and view a short flash video. VERIFICATION: Did the video play correctly? plugin: user-interact-verify id: software/firefox-totem depends: software/firefox requires: package.name == 'firefox' package.name == 'gstreamer0.10-ffmpeg' command: firefox $PLAINBOX_PROVIDER_DATA/video/Quicktime_Video.mov _description: PURPOSE: This test will check that Firefox can play a Quicktime (.mov) video file. Note: this may require installing additional software to successfully complete. STEPS: 1. Select Test to launch Firefox with a sample video. VERIFICATION: Did the video play using a plugin? plugin: user-interact-verify id: software/empathy-facebook_chat requires: package.name == "empathy" command: empathy _description: PURPOSE: This test will check that Empathy messaging client works. STEPS: 1. Select Test to launch Empathy. 2. Configure it to connect to the Facebook Chat service. 3. Once you have completed the test, please quit Empathy to continue here. VERIFICATION: Were you able to connect correctly and send/receive messages? plugin: user-interact-verify id: software/empathy-google_talk requires: package.name == "empathy" command: empathy _description: PURPOSE: This test will check that Empathy messaging client works. STEPS: 1. Select Test to launch Empathy. 2. Configure it to connect to the Google Talk (gtalk) service. 3. Once you have completed the test, please quit Empathy to continue here. VERIFICATION: Were you able to connect correctly and send/receive messages? plugin: user-interact-verify id: software/empathy-jabber requires: package.name == "empathy" command: empathy _description: PURPOSE: This test will check that Empathy messaging client works. STEPS: 1. Select Test to launch Empathy. 2. Configure it to connect to the Jabber service. 3. Once you have completed the test, please quit Empathy to continue here. VERIFICATION: Were you able to connect correctly and send/receive messages? plugin: user-interact-verify id: software/empathy-aim requires: package.name == "empathy" command: empathy _description: PURPOSE: This test will check that Empathy messaging client works. STEPS: 1. Select Test to launch Empathy. 2. Configure it to connect to the AOL Instant Messaging (AIM) service. 3. Once you have completed the test, please quit Empathy to continue here. VERIFICATION: Were you able to connect correctly and send/receive messages? plugin: user-interact-verify id: software/empathy-msn requires: package.name == "empathy" command: empathy _description: PURPOSE: This test will check that Empathy messaging client works. STEPS: 1. Select Test to launch Empathy. 2. Configure it to connect to the Microsoft Network (MSN) service. 3. Once you have completed the test, please quit Empathy to continue here. VERIFICATION: Were you able to connect correctly and send/receive messages? plugin: user-interact-verify id: software/evolution-pop3 requires: package.name == "evolution" command: evolution _description: PURPOSE: This test will check that Evolution works. STEPS: 1. Click the "Test" button to launch Evolution. 2. Configure it to connect to a POP3 account. VERIFICATION: Were you able to receive and read e-mail correctly? plugin: user-interact-verify id: software/evolution-imap requires: package.name == "evolution" command: evolution _description: PURPOSE: This test will check that Evolution works. STEPS: 1. Click the "Test" button to launch Evolution. 2. Configure it to connect to a IMAP account. VERIFICATION: Were you able to receive and read e-mail correctly? plugin: user-interact-verify id: software/evolution-smtp requires: package.name == "evolution" command: evolution _description: PURPOSE: This test will check that Evolution works. STEPS: 1. Click the "Test" button to launch Evolution. 2. Configure it to connect to a SMTP account. VERIFICATION: Were you able to send e-mail without errors? plugin: user-interact-verify id: software/gcalctool requires: package.name == "gcalctool" command: gcalctool _description: PURPOSE: This test checks that gcalctool (Calculator) works. STEPS: Click the "Test" button to open the calculator. VERIFICATION: Did it launch correctly? plugin: user-interact-verify id: software/gcalctool-functions depends: software/gcalctool requires: package.name == "gcalctool" command: gcalctool _description: PURPOSE: This test checks that gcalctool (Calculator) works. STEPS: Click the "Test" button to open the calculator and perform: 1. Simple math functions (+,-,/,*) 2. Nested math functions ((,)) 3. Fractional math 4. Decimal math VERIFICATION: Did the functions perform as expected? plugin: user-interact-verify id: software/gcalctool-memory depends: software/gcalctool requires: package.name == "gcalctool" command: gcalctool _description: PURPOSE: This test checks that gcalctool (Calculator) works. STEPS: Click the "Test" button to open the calculator and perform: 1. Memory set 2. Memory reset 3. Memory last clear 4. Memory clear VERIFICATION: Did the functions perform as expected? plugin: user-interact-verify id: software/gcalctool-clipboard depends: software/gcalctool requires: package.name == "gcalctool" command: gcalctool _description: PURPOSE: This test checks that gcalctool (Calculator) works. STEPS: Click the "Test" button to open the calculator and perform: 1. Cut 2. Copy 3. Paste VERIFICATION: Did the functions perform as expected? plugin: user-interact-verify id: software/gedit requires: package.name == "gedit" command: gedit _description: PURPOSE: This test checks that gedit works. STEPS: 1. Click the "Test" button to open gedit. 2. Enter some text and save the file (make a note of the file name you use), then close gedit. VERIFICATION: Did this perform as expected? plugin: user-interact-verify id: software/gedit-read depends: software/gedit requires: package.name == "gedit" command: gedit _description: PURPOSE: This test checks that gedit works. STEPS: 1. Click the "Test" button to open gedit, and re-open the file you created previously. 2. Edit then save the file, then close gedit. VERIFICATION: Did this perform as expected? plugin: user-interact-verify id: software/gnome-terminal requires: package.name == "gnome-terminal" command: gnome-terminal _description: PURPOSE: This test will check that Gnome Terminal works. STEPS: 1. Click the "Test" button to open Terminal. 2. Type 'ls' and press enter. You should see a list of files and folder in your home directory. 3. Close the terminal window. VERIFICATION: Did this perform as expected? 2013.com.canonical.certification.checkbox-0.4/jobs/input.txt.in0000664000175000017500000000367612320565736024313 0ustar zygazyga00000000000000plugin: local id: input/pointing requires: device.category == 'MOUSE' or device.category == 'TOUCHPAD' or device.category == 'TOUCHSCREEN' _description: Pointing device tests. command: cat <<'EOF' | run_templates -t -s 'udev_resource | filter_templates -w "category=MOUSE" -w "category=TOUCHPAD" -w "category=TOUCHSCREEN"' plugin: manual id: input/pointing_`echo "${product}_${category}" | sed 's/ /_/g;s/[^_a-zA-Z0-9-]//g'`_`basename $path` requires: device.path == "$path" description: PURPOSE: This will test your $product device STEPS: 1. Move the cursor with your $product. VERIFICATION: Did the cursor move? EOF plugin: manual id: input/mouse _description: PURPOSE: This test will test your pointing device STEPS: 1. Move the cursor using the pointing device or touch the screen. 2. Perform some single/double/right click operations. VERIFICATION: Did the pointing device work as expected? plugin: user-interact-verify id: input/keyboard command: keyboard_test requires: device.category == 'KEYBOARD' _description: PURPOSE: This test will test your keyboard STEPS: 1. Click on Test 2. On the open text area, use your keyboard to type something VERIFICATION: Is your keyboard working properly? plugin: manual id: input/accelerometer_verify _description: PURPOSE: Manual detection of accelerometer. STEPS: 1. Look at the specifications for your system. VERIFICATION: Is this system supposed to have an accelerometer? plugin: user-interact id: input/accelerometer user: root depends: input/accelerometer_verify command: accelerometer_test -m _description: PURPOSE: This test will test your accelerometer to see if it is detected and operational as a joystick device. STEPS: 1. Click on Test 2. Tilt your hardware in the directions onscreen until the axis threshold is met. VERIFICATION: Is your accelerometer properly detected? Can you use the device? 2013.com.canonical.certification.checkbox-0.4/whitelists/0000775000175000017500000000000012320541307023220 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/whitelists/sru.whitelist0000664000175000017500000000552112320541306025771 0ustar zygazyga00000000000000## This whitelist contains tests that are useful for validating a Stable ## Release Update (SRU) on Ubuntu Certified systems. This whitelist is not ## recommended for, nor will it be accepted for self-testing purposes. IEEE_80211 block_device cdimage cpuinfo device dmi dpkg efi environment gconf lsb meminfo module package rtc sleep uname xinput #File Attachment Jobs __info__ acpi_sleep_attachment codecs_attachment cpuinfo_attachment dmesg_attachment dmi_attachment dmidecode_attachment efi_attachment lspci_attachment meminfo_attachment modprobe_attachment modules_attachment sysctl_attachment sysfs_attachment udev_attachment lsmod_attachment #SRU Test Suite Jobs __audio__ audio/alsa_record_playback_automated __bluetooth__ bluetooth/detect-output __camera__ camera/detect camera/multiple-resolution-images __cpu__ cpu/scaling_test cpu/scaling_test-log-attach cpu/offlining_test cpu/topology __disk__ disk/read_performance disk/read_performance_.* __graphics__ graphics/xorg-version graphics/compiz_check graphics/xorg-failsafe graphics/xorg-process graphics/screenshot screenshot.jpg graphics/screenshot_fullscreen_video screenshot_fullscreen_video.jpg graphics/screenshot_opencv_validation screenshot_opencv_validation.jpg __install__ install/apt-get-gets-updates __mediacard__ mediacard/sd-preinserted __memory__ memory/info memory/check __ethernet__ ethernet/detect ethernet/info_automated __networking__ networking/http networking/gateway_ping __power-management__ power-management/tickless_idle power-management/rtc power-management/fwts_wakealarm power-management/fwts_wakealarm-log-attach __usb__ usb/detect usb/storage-preinserted __wireless__ wireless/wireless_scanning wireless/wireless_connection_wpa_bg wireless/wireless_connection_open_bg wireless/wireless_connection_wpa_n wireless/wireless_connection_open_n wireless/monitor_wireless_connection_udp __suspend__ suspend/audio_before_suspend suspend/bluetooth_obex_send_before_suspend suspend/bluetooth_obex_browse_before_suspend suspend/bluetooth_obex_get_before_suspend suspend/cpu_before_suspend suspend/network_before_suspend suspend/memory_before_suspend suspend/suspend_advanced_auto suspend/suspend-single-log-check suspend/audio_after_suspend_auto suspend/network_resume_time_auto suspend/wifi_resume_time_auto suspend/usb_storage_preinserted_after_suspend suspend/record_playback_after_suspend_auto suspend/bluetooth_obex_send_after_suspend_auto suspend/bluetooth_obex_browse_after_suspend_auto suspend/bluetooth_obex_get_after_suspend_auto suspend/cpu_after_suspend_auto suspend/memory_after_suspend_auto suspend/wireless_connection_after_suspend_wpa_bg_auto suspend/wireless_connection_after_suspend_open_bg_auto suspend/wireless_connection_after_suspend_wpa_n_auto suspend/wireless_connection_after_suspend_open_n_auto suspend/gpu_lockup_after_suspend suspend/screenshot_after_suspend screenshot_after_suspend.jpg 2013.com.canonical.certification.checkbox-0.4/whitelists/hwsubmit.whitelist0000664000175000017500000000043012320541306027014 0ustar zygazyga00000000000000cdimage cpuinfo device dmi dpkg gconf lsb meminfo module package uname usb __info__ cpuinfo_attachment dmesg_attachment dmi_attachment dmidecode_attachment lsmod_attachment lspci_attachment modprobe_attachment modules_attachment sysfs_attachment sysctl_attachment udev_attachment 2013.com.canonical.certification.checkbox-0.4/whitelists/default.whitelist0000664000175000017500000000517412320541306026610 0ustar zygazyga00000000000000optical_drive __miscellanea__ miscellanea/submission-resources miscellanea/is_laptop __info__ codecs_attachment cpuinfo_attachment dmesg_attachment dmi_attachment dmidecode_attachment lshw_attachment efi_attachment lsmod_attachment lspci_attachment modprobe_attachment modules_attachment sysfs_attachment sysctl_attachment udev_attachment __audio__ audio/list_devices audio/playback_headphones audio/playback_auto audio/alsa_record_playback_external audio/alsa_record_playback_internal audio/alsa_record_playback_usb audio/playback_auto __benchmarks__ benchmarks/disk/hdparm-read benchmarks/disk/hdparm-read_.* benchmarks/disk/hdparm-cache-read benchmarks/disk/hdparm-cache-read_.* __bluetooth__ bluetooth/detect-output __camera__ camera/detect camera/still camera/display __cpu__ cpu/offlining_test cpu/topology cpu/clocktest __disk__ disk/detect __firewire__ firewire/insert firewire/storage-test firewire/remove __graphics__ graphics/compiz_check graphics/display graphics/resolution graphics/minimum_resolution graphics/driver_version graphics/VESA_drivers_not_in_use __input__ input/keyboard input/mouse __keys__ keys/super keys/battery-info keys/brightness keys/media-control keys/mute keys/volume keys/wireless __mediacard__ mediacard/mmc-insert mediacard/mmc-storage mediacard/mmc-remove mediacard/sd-insert mediacard/sd-storage mediacard/sd-remove mediacard/sdhc-insert mediacard/sdhc-storage mediacard/sdhc-remove mediacard/cf-insert mediacard/cf-storage mediacard/cf-remove __memory__ memory/info __monitor__ monitor/hdmi monitor/vga monitor/powersaving __ethernet__ ethernet/detect __wireless__ wireless/wireless_connection __optical__ optical/detect optical/read optical/read_.* __expresscard__ expresscard/verification __power-management__ power-management/rtc __suspend__ suspend/audio_before_suspend suspend/memory_before_suspend suspend/network_before_suspend suspend/resolution_before_suspend suspend/suspend_advanced suspend/wireless_before_suspend # Test sleep key after we ensured suspend works keys/sleep suspend/mmc-insert-after-suspend suspend/mmc-storage-after-suspend suspend/mmc-remove-after-suspend suspend/sd-insert-after-suspend suspend/sd-storage-after-suspend suspend/sd-remove-after-suspend suspend/sdhc-insert-after-suspend suspend/sdhc-storage-after-suspend suspend/sdhc-remove-after-suspend mediacard/cf-insert-after-suspend mediacard/cf-storage-after-suspend mediacard/cf-remove-after-suspend suspend/audio_after_suspend suspend/bluetooth_detect_after_suspend suspend/memory_after_suspend suspend/network_after_suspend suspend/resolution_after_suspend suspend/wireless_after_suspend __usb__ usb/detect usb/insert usb/storage-automated usb/remove 2013.com.canonical.certification.checkbox-0.4/whitelists/sniff.whitelist0000664000175000017500000000124012320541306026257 0ustar zygazyga00000000000000## This is an example whitelist to start from. ## To use, copy this file and add the jobs you want to run to the copy. Delete ## these comments. DO NOT delete the first 9 jobs in tis file. They are resource ## gathering jobs and are necessary to do any testing at all. # Resource Jobs (listed in jobs/resource.txt) cdimage cpuinfo device dmi dpkg efi environment gconf lsb meminfo module package rtc sleep uname # Smoke sniffcases __info__ info/audio_device_driver __sniff__ sniff/sniff7 sniff/sniff6 sniff/sniff5 sniff/sniff4 sniff/sniff3 sniff/sniff2 sniff/sniff1 __power-management__ power-management/rtc __suspend__ suspend/suspend_advanced suspend/suspend-time-check 2013.com.canonical.certification.checkbox-0.4/whitelists/autotesting.whitelist0000664000175000017500000000567612320541306027541 0ustar zygazyga00000000000000## This whitelist is used for automated testing of the test cases themselves. # These resources are needed for a valid submission, so they are included # explicitly, even if they are usually run implicitly by jobs requiring them. IEEE_80211 cpuinfo dpkg lsb package uname #File Attachment Jobs __info__ acpi_sleep_attachment codecs_attachment cpuinfo_attachment dmesg_attachment dmi_attachment dmidecode_attachment efi_attachment lspci_attachment meminfo_attachment modprobe_attachment modules_attachment sysctl_attachment sysfs_attachment udev_attachment lsmod_attachment #Automated Test-verification Jobs __audio__ audio/alsa_record_playback_automated __bluetooth__ bluetooth/detect-output __camera__ camera/detect camera/multiple-resolution-images __cpu__ cpu/scaling_test cpu/scaling_test-log-attach cpu/offlining_test cpu/topology __disk__ disk/read_performance disk/read_performance_.* __graphics__ graphics/xorg-version graphics/compiz_check graphics/xorg-failsafe graphics/xorg-process graphics/screenshot screenshot.jpg graphics/screenshot_fullscreen_video screenshot_fullscreen_video.jpg __install__ install/apt-get-gets-updates __mediacard__ mediacard/sd-preinserted __memory__ memory/info memory/check __miscellanea__ miscellanea/submission-resources miscellanea/fwts_test miscellanea/fwts_results.log __ethernet__ ethernet/detect networking/http networking/gateway_ping __power-management__ power-management/tickless_idle power-management/rtc power-management/fwts_wakealarm power-management/fwts_wakealarm-log-attach __usb__ usb/detect usb/storage-preinserted __wireless__ wireless/wireless_scanning wireless/wireless_connection_wpa_bg wireless/wireless_connection_open_bg wireless/wireless_connection_wpa_n wireless/wireless_connection_open_n wireless/wireless_connection_wpa_ac wireless/wireless_connection_open_ac wireless/monitor_wireless_connection_udp __suspend__ suspend/audio_before_suspend suspend/bluetooth_obex_send_before_suspend suspend/bluetooth_obex_browse_before_suspend suspend/bluetooth_obex_get_before_suspend suspend/cpu_before_suspend suspend/network_before_suspend suspend/memory_before_suspend suspend/suspend_advanced_auto suspend/suspend-single-log-check suspend/audio_after_suspend_auto suspend/network_resume_time_auto suspend/wifi_resume_time_auto suspend/usb_storage_preinserted_after_suspend suspend/record_playback_after_suspend_auto suspend/bluetooth_obex_send_after_suspend_auto suspend/bluetooth_obex_browse_after_suspend_auto suspend/bluetooth_obex_get_after_suspend_auto suspend/cpu_after_suspend_auto suspend/memory_after_suspend_auto suspend/wireless_connection_after_suspend_wpa_bg_auto suspend/wireless_connection_after_suspend_open_bg_auto suspend/wireless_connection_after_suspend_wpa_n_auto suspend/wireless_connection_after_suspend_open_n_auto suspend/wireless_connection_after_suspend_wpa_ac_auto suspend/wireless_connection_after_suspend_open_ac_auto suspend/gpu_lockup_after_suspend suspend/screenshot_after_suspend screenshot_after_suspend.jpg 2013.com.canonical.certification.checkbox-0.4/whitelists/smoke.whitelist0000664000175000017500000000105312320541306026272 0ustar zygazyga00000000000000## This is an example whitelist to start from. ## To use, copy this file and add the jobs you want to run to the copy. Delete ## these comments. DO NOT delete the first 9 jobs in tis file. They are resource ## gathering jobs and are necessary to do any testing at all. # Resource Jobs (listed in jobs/resource.txt) cpuinfo cdimage dmi dpkg efi environment gconf lsb meminfo module package device uname # Smoke test cases __smoke__ smoke/true smoke/false smoke/dependency/good smoke/dependency/bad smoke/requirement/good smoke/requirement/bad smoke/manual 2013.com.canonical.certification.checkbox-0.4/bin/0000775000175000017500000000000012320567463021604 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/bin/gcov_tarball0000775000175000017500000000023012320541306024150 0ustar zygazyga00000000000000#!/bin/sh set -o errexit cd /usr/share tar -xzf gcov.tar.gz cd /tmp lcov -q -c -o gcov.info genhtml -q -o gcov gcov.info 2>/dev/null tar -czf - gcov 2013.com.canonical.certification.checkbox-0.4/bin/accelerometer_test0000775000175000017500000002745512320541306025404 0ustar zygazyga00000000000000#!/usr/bin/env python3 ''' script to test accerometer functionality Copyright (C) 2012 Canonical Ltd. Authors Jeff Marcom This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . The purpose of this script is to simply interact with an onboard accelerometer, and check to be sure that the x, y, z axis respond to physical movement of hardware. ''' from argparse import ArgumentParser from gi.repository import Gdk, GLib, Gtk import logging import os import re import sys import threading import time from subprocess import Popen, PIPE, check_output, STDOUT, CalledProcessError from checkbox_support.parsers.modinfo import ModinfoParser handler = logging.StreamHandler() logger = logging.getLogger() logger.addHandler(handler) logger.setLevel(logging.DEBUG) class AccelerometerUI(Gtk.Window): """Builds UI Framework for axis threshold tests using Gtk""" def __init__(self): Gtk.Window.__init__(self, title="Accelerometer Test") self.set_default_size(450, 100) self.set_type_hint(Gdk.WindowType.TOPLEVEL) self.enabled = False # Create UI Grid w_table = Gtk.Grid() self.add(w_table) # Create axis buttons self.up_icon = Gtk.Image(stock=Gtk.STOCK_GO_UP) self.up_icon.set_padding(10, 30) self.down_icon = Gtk.Image(stock=Gtk.STOCK_GO_DOWN) self.down_icon.set_padding(10, 30) self.left_icon = Gtk.Image(stock=Gtk.STOCK_GO_BACK) self.right_icon = Gtk.Image(stock=Gtk.STOCK_GO_FORWARD) # Set debug self.debug_label = Gtk.Label("Debug") # Set Grid layout for UI message = "Please tilt your hardware in the positions shown below:" w_table.attach(Gtk.Label(message), 0, 0, 4, 1) w_table.attach(self.up_icon, 2, 2, 1, 1) w_table.attach_next_to(self.debug_label, self.up_icon, Gtk.PositionType.BOTTOM, 1, 1) w_table.attach_next_to(self.down_icon, self.debug_label, Gtk.PositionType.BOTTOM, 1, 1) w_table.attach_next_to(self.left_icon, self.debug_label, Gtk.PositionType.LEFT, 1, 1) w_table.attach_next_to(self.right_icon, self.debug_label, Gtk.PositionType.RIGHT, 1, 1) def update_axis_icon(self, direction): """Change desired directional icon to checkmark""" exec('self.%s_icon.set_from_stock' % (direction) \ + '(Gtk.STOCK_YES, size=Gtk.IconSize.BUTTON)') def update_debug_label(self, text): """Update axis information in center of UI""" self.debug_label.set_text(text) def destroy(self): Gtk.main_quit() def enable(self): self.enabled = True self.connect("delete-event", Gtk.main_quit) self.show_all() # Enable GLib/Gdk threading so the UI won't lock main GLib.threads_init() Gdk.threads_init() Gdk.threads_enter() Gtk.main() Gdk.threads_leave() class PermissionException(RuntimeError): def __init__(self, error): message = "Please re-run with root permissions: %s" % error.strip() super(PermissionException, self).__init__(message) class AxisData(threading.Thread): """Acquire information from kernel regarding the state of the accelerometer axis positions. Gathered data will be compared to a preset threshold reading. The default threshold (either - or + ) for any direction is 600. Return values for thread are SUCCESS:0 FAILURE:1. FAILURE is likely to exists when thread is unable to obtain a valid reading from the hardware.""" def __init__(self, device_path, ui_control=None): threading.Thread.__init__(self) self.ui = ui_control self.device_path = device_path.strip("/") self.tilt_threshold = 600 self.x_test_pool = ["up", "down"] self.y_test_pool = ["left", "right"] if self.ui == None: self.ui.enabled = False def grab_current_readings(self): """Search device path and return axis tuple""" time.sleep(0.5) # Sleep to accomodate slower processors data_file = os.path.join("/sys", self.device_path, "device", "position") # Try and retrieve positional data from kernel try: position_tuple = open(data_file) except (OSError, IOError): logging.error("Failed to open: %s" % data_file) return False # Split data for x, y, z as it's easier to manage threshold tests. axis_set = position_tuple.read().strip("\n()") return axis_set.split(",") def parse_reading(self, value, mapping): """Check for positive or negative threshold match""" if abs(value) >= abs(self.tilt_threshold): # And return test pool array position based on integer if value < 0: return 2 return 1 def direction_poll(self, x_axis, y_axis): """Poll for threshold being met per x, and y axis""" direction_map = {"X": x_axis, "Y": y_axis} for mapping, data in direction_map.items(): reading = self.parse_reading(int(data), mapping) if type(reading) == int: return reading, mapping # Return nothing if threshold is not met return False, None def run(self): rem_tests = self.y_test_pool + self.x_test_pool while len(rem_tests) > 0: axis_data_bundle = self.grab_current_readings() if type(axis_data_bundle) != list: logging.error("Failed to grab appropriate readings") return 1 # Parse for current positional values # Hdaps will only report X, and Y positional data x_data = int(axis_data_bundle[0]) y_data = int(axis_data_bundle[1]) if len(axis_data_bundle) > 2: z_data = int(axis_data_bundle[2]) else: z_data = 0 debug_info = "X: %s Y: %s Z: %s" % (x_data, y_data, z_data) if self.ui.enabled: # Update positional values in UI self.ui.update_debug_label(debug_info) position, axis = self.direction_poll(x_data, y_data) if position: # Check axis set and delete test from pool if axis == "X": pool = self.x_test_pool else: pool = self.y_test_pool if len(pool) >= position: direction = pool[position - 1] if direction in rem_tests: # Remove direction from test pool del rem_tests[rem_tests.index(direction)] self.ui.update_axis_icon(direction) else: # Accept readings as successful test result logging.debug("Latest Readings: %s" % debug_info) break if self.ui.enabled: self.ui.destroy() return 0 def insert_supported_module(oem_module): """Try and insert supported module to see if we get any init errors""" try: stream = check_output(['modinfo', oem_module], stderr=STDOUT, universal_newlines=True) except CalledProcessError as err: print("Error accessing modinfo for %s: " % oem_module, file=sys.stderr) print(err.output, file=sys.stderr) return err.returncode parser = ModinfoParser(stream) module = os.path.basename(parser.get_field('filename')) insmod_output = Popen(['insmod %s' % module], stderr=PIPE, shell=True, universal_newlines=True) error = insmod_output.stderr.read() if "Permission denied" in error: raise PermissionException(error) return insmod_output.returncode def check_module_status(): """Looks to see if it can determine the hardware manufacturer and report corresponding accelerometer driver status""" oem_driver_pool = {"hewlett-packard": "hp_accel", "toshiba": "hp_accel", "ibm": "hdaps", "lenovo": "hdaps"} oem_module = None dmi_info = Popen(['dmidecode'], stdout=PIPE, stderr=PIPE, universal_newlines=True) output, error = dmi_info.communicate() if "Permission denied" in error: raise PermissionException(error) vendor_data = re.findall("Vendor:\s.*", output) try: manufacturer = vendor_data[0].split(":")[1].strip() except IndexError as exception: logging.error("Failed to find Manufacturing data") return logging.debug(manufacturer) # Now we look to see if there was any info during boot # time that would help in debugging this failure for vendor, module in oem_driver_pool.items(): if manufacturer.lower() == vendor: oem_module = oem_driver_pool.get(vendor) break # We've found our desired module to probe. if oem_module != None: if insert_supported_module(oem_module) != None: logging.error("Failed module insertion") # Check dmesg status for supported module driver_status = Popen(['dmesg'], stdout=PIPE, universal_newlines=True) module_regex = oem_module + ".*" kernel_notes = re.findall(module_regex, driver_status.stdout.read()) # Report ALL findings, it's useful to note it the driver failed init # more than once of actually passed despite a reading failure logging.debug("\n".join((kernel_notes))) else: logging.error("No supported module") def check_for_accelerometer(): """Checks device list for existence of accelerometer and returns name, manufacturer, and system path info.""" found = False device_info = open("/proc/bus/input/devices").readlines() for line in device_info: if "accelerometer" in line.lower(): target = device_info.index(line) name = device_info[target].split("=")[1] path = device_info[target + 2].split("=")[1] found = True break if found: logger.debug("Name: %s\nPath: %s" % (name, path)) return path.strip() else: # Return False as it's expected logger.error("Accelerometer hardware not found") return False def main(): parser = ArgumentParser(description="Tests accelerometer functionality") parser.add_argument('-m', '--manual', default=False, action='store_true', help="For manual test with visual notification") parser.add_argument('-a', '--automated', default=True, action='store_true', help="For automated test using defined parameters") args = parser.parse_args() sys_path = check_for_accelerometer() if not sys_path: try: check_module_status() except PermissionException as error: print(error, file=sys.stderr) sys.exit(1) ui = AccelerometerUI() grab_data = AxisData(sys_path, ui) grab_data.setDaemon(True) grab_data.start() if args.manual: ui.enable() else: # Sleep for enough time to retrieve a reading. # Reading is not instant. time.sleep(5) if __name__ == '__main__': main(); 2013.com.canonical.certification.checkbox-0.4/bin/removable_storage_watcher0000775000175000017500000012723512320541306026745 0ustar zygazyga00000000000000#!/usr/bin/env python3 import argparse import collections import copy import dbus import logging import sys from gi.repository import GObject, GUdev from checkbox_support.dbus import connect_to_system_bus from checkbox_support.dbus.udisks2 import UDisks2Model, UDisks2Observer from checkbox_support.dbus.udisks2 import is_udisks2_supported from checkbox_support.dbus.udisks2 import lookup_udev_device from checkbox_support.dbus.udisks2 import map_udisks1_connection_bus from checkbox_support.heuristics.udisks2 import is_memory_card from checkbox_support.parsers.udevadm import CARD_READER_RE, GENERIC_RE, FLASH_RE from checkbox_support.udev import get_interconnect_speed, get_udev_block_devices # Record representing properties of a UDisks1 Drive object needed by the # UDisks1 version of the watcher implementation UDisks1DriveProperties = collections.namedtuple( 'UDisks1DriveProperties', 'file bus speed model vendor media') # Delta record that encapsulates difference: # delta_dir -- directon of the difference, either DELTA_DIR_PLUS or # DELTA_DIR_MINUS # value -- the actual value being removed or added, either InterfaceDelta or # PropertyDelta instance, see below DeltaRecord = collections.namedtuple("DeltaRecord", "delta_dir value") # Delta value for representing interface changes InterfaceDelta = collections.namedtuple( "InterfaceDelta", "delta_type object_path iface_name") # Delta value for representing property changes PropertyDelta = collections.namedtuple( "PropertyDelta", "delta_type object_path iface_name prop_name prop_value") # Tokens that encode additions and removals DELTA_DIR_PLUS = '+' DELTA_DIR_MINUS = '-' # Tokens that encode interface and property deltas DELTA_TYPE_IFACE = 'i' DELTA_TYPE_PROP = 'p' def format_bytes(size): """ Format size to be easily read by humans The result is disk-size compatible (using multiples of 10 rather than 2) string like "4.5GB" """ for index, prefix in enumerate(" KMGTPEZY", 0): factor = 10 ** (index * 3) if size // factor <= 1000: break return "{}{}B".format(size // factor, prefix.strip()) class UDisks1StorageDeviceListener: def __init__(self, system_bus, loop, action, devices, minimum_speed, memorycard): self._action = action self._devices = devices self._minimum_speed = minimum_speed self._memorycard = memorycard self._bus = system_bus self._loop = loop self._error = False self._change_cache = [] def check(self, timeout): udisks = 'org.freedesktop.UDisks' if self._action == 'insert': signal = 'DeviceAdded' logging.debug("Adding signal listener for %s.%s", udisks, signal) self._bus.add_signal_receiver(self.add_detected, signal_name=signal, dbus_interface=udisks) elif self._action == 'remove': signal = 'DeviceRemoved' logging.debug("Adding signal listener for %s.%s", udisks, signal) self._bus.add_signal_receiver(self.remove_detected, signal_name=signal, dbus_interface=udisks) self._starting_devices = self.get_existing_devices() logging.debug("Starting with the following devices: %r", self._starting_devices) def timeout_callback(): print("%s seconds have expired " "waiting for the device to be inserted." % timeout) self._error = True self._loop.quit() logging.debug("Adding timeout listener, timeout=%r", timeout) GObject.timeout_add_seconds(timeout, timeout_callback) logging.debug("Starting event loop...") self._loop.run() return self._error def verify_device_change(self, changed_devices, message=""): logging.debug("Verifying device change: %s", changed_devices) # Filter the applicable bus types, as provided on the command line # (values of self._devices can be 'usb', 'firewire', etc) desired_bus_devices = [ device for device in changed_devices if device.bus in self._devices] logging.debug("Desired bus devices: %s", desired_bus_devices) for dev in desired_bus_devices: if self._memorycard: if (dev.bus != 'sdio' and not FLASH_RE.search(dev.media) and not CARD_READER_RE.search(dev.model) and not GENERIC_RE.search(dev.vendor)): logging.debug("The device does not seem to be a memory" " card (bus: %r, model: %r), skipping", dev.bus, dev.model) return print(message % {'bus': 'memory card', 'file': dev.file}) else: if (FLASH_RE.search(dev.media) or CARD_READER_RE.search(dev.model) or GENERIC_RE.search(dev.vendor)): logging.debug("The device seems to be a memory" " card (bus: %r (model: %r), skipping", dev.bus, dev.model) return print(message % {'bus': dev.bus, 'file': dev.file}) if self._minimum_speed: if dev.speed >= self._minimum_speed: print("with speed of %(speed)s bits/s " "higher than %(min_speed)s bits/s" % {'speed': dev.speed, 'min_speed': self._minimum_speed}) else: print("ERROR: speed of %(speed)s bits/s lower " "than %(min_speed)s bits/s" % {'speed': dev.speed, 'min_speed': self._minimum_speed}) self._error = True logging.debug("Device matches requirements, exiting event loop") self._loop.quit() def job_change_detected(self, devices, job_in_progress, job_id, job_num_tasks, job_cur_task_id, job_cur_task_percentage): logging.debug("UDisks1 reports a job change has been detected:" " devices: %s, job_in_progress: %s, job_id: %s," " job_num_tasks: %s, job_cur_task_id: %s," " job_cur_task_percentage: %s", devices, job_in_progress, job_id, job_num_tasks, job_cur_task_id, job_cur_task_percentage) if job_id == "FilesystemMount": if devices in self._change_cache: logging.debug("Ignoring filesystem mount," " the device is present in change cache") return logging.debug("Adding devices to change cache: %r", devices) self._change_cache.append(devices) logging.debug("Starting devices were: %s", self._starting_devices) current_devices = self.get_existing_devices() logging.debug("Current devices are: %s", current_devices) inserted_devices = list(set(current_devices) - set(self._starting_devices)) logging.debug("Computed inserted devices: %s", inserted_devices) if self._memorycard: message = "Expected memory card device %(file)s inserted" else: message = "Expected %(bus)s device %(file)s inserted" self.verify_device_change(inserted_devices, message=message) def add_detected(self, added_path): logging.debug("UDisks1 reports device has been added: %s", added_path) logging.debug("Resetting change_cache to []") self._change_cache = [] signal_name = 'DeviceJobChanged' dbus_interface = 'org.freedesktop.UDisks' logging.debug("Adding signal listener for %s.%s", dbus_interface, signal_name) self._bus.add_signal_receiver(self.job_change_detected, signal_name=signal_name, dbus_interface=dbus_interface) def remove_detected(self, removed_path): logging.debug("UDisks1 reports device has been removed: %s", removed_path) logging.debug("Starting devices were: %s", self._starting_devices) current_devices = self.get_existing_devices() logging.debug("Current devices are: %s", current_devices) removed_devices = list(set(self._starting_devices) - set(current_devices)) logging.debug("Computed removed devices: %s", removed_devices) self.verify_device_change(removed_devices, message="Removable %(bus)s device %(file)s has been removed") def get_existing_devices(self): logging.debug("Getting existing devices from UDisks1") ud_manager_obj = self._bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks') existing_devices = [] for dev in ud_manager.EnumerateDevices(): try: device_obj = self._bus.get_object("org.freedesktop.UDisks", dev) device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) udisks = 'org.freedesktop.UDisks.Device' _device_file = device_props.Get(udisks, "DeviceFile") _bus = device_props.Get(udisks, "DriveConnectionInterface") _speed = device_props.Get(udisks, "DriveConnectionSpeed") _parent_model = '' _parent_media = '' _parent_vendor = '' if device_props.Get(udisks, "DeviceIsPartition"): parent_obj = self._bus.get_object( "org.freedesktop.UDisks", device_props.Get(udisks, "PartitionSlave")) parent_props = dbus.Interface( parent_obj, dbus.PROPERTIES_IFACE) _parent_model = parent_props.Get(udisks, "DriveModel") _parent_vendor = parent_props.Get(udisks, "DriveVendor") _parent_media = parent_props.Get(udisks, "DriveMedia") if not device_props.Get(udisks, "DeviceIsDrive"): device = UDisks1DriveProperties( file=str(_device_file), bus=str(_bus), speed=int(_speed), model=str(_parent_model), vendor=str(_parent_vendor), media=str(_parent_media)) existing_devices.append(device) except dbus.DBusException: pass return existing_devices def udisks2_objects_delta(old, new): """ Compute the delta between two snapshots of udisks2 objects The objects are encoded as {s:{s:{s:v}}} where the first dictionary maps from DBus object path to a dictionary that maps from interface name to a dictionary that finally maps from property name to property value. The result is a generator of DeltaRecord objects that encodes the changes: * the 'delta_dir' is either DELTA_DIR_PLUS or DELTA_DIR_MINUS * the 'value' is a tuple that differs for interfaces and properties. Interfaces use the format (DELTA_TYPE_IFACE, object_path, iface_name) while properties use the format (DELTA_TYPE_PROP, object_path, iface_name, prop_name, prop_value) Interfaces are never "changed", they are only added or removed. Properties can be changed and this is encoded as removal followed by an addition where both differ only by the 'delta_dir' and the last element of the 'value' tuple. """ # Traverse all objects, old or new all_object_paths = set() all_object_paths |= old.keys() all_object_paths |= new.keys() for object_path in sorted(all_object_paths): old_object = old.get(object_path, {}) new_object = new.get(object_path, {}) # Traverse all interfaces of each object, old or new all_iface_names = set() all_iface_names |= old_object.keys() all_iface_names |= new_object.keys() for iface_name in sorted(all_iface_names): if iface_name not in old_object and iface_name in new_object: # Report each ADDED interface assert iface_name in new_object delta_value = InterfaceDelta( DELTA_TYPE_IFACE, object_path, iface_name) yield DeltaRecord(DELTA_DIR_PLUS, delta_value) # Report all properties ADDED on that interface for prop_name, prop_value in new_object[iface_name].items(): delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path, iface_name, prop_name, prop_value) yield DeltaRecord(DELTA_DIR_PLUS, delta_value) elif iface_name not in new_object and iface_name in old_object: # Report each REMOVED interface assert iface_name in old_object delta_value = InterfaceDelta( DELTA_TYPE_IFACE, object_path, iface_name) yield DeltaRecord(DELTA_DIR_MINUS, delta_value) # Report all properties REMOVED on that interface for prop_name, prop_value in old_object[iface_name].items(): delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path, iface_name, prop_name, prop_value) yield DeltaRecord(DELTA_DIR_MINUS, delta_value) else: # Analyze properties of each interface that existed both in old # and new object trees. assert iface_name in new_object assert iface_name in old_object old_props = old_object[iface_name] new_props = new_object[iface_name] all_prop_names = set() all_prop_names |= old_props.keys() all_prop_names |= new_props.keys() # Traverse all properties, old or new for prop_name in sorted(all_prop_names): if prop_name not in old_props and prop_name in new_props: # Report each ADDED property delta_value = PropertyDelta( DELTA_TYPE_PROP, object_path, iface_name, prop_name, new_props[prop_name]) yield DeltaRecord(DELTA_DIR_PLUS, delta_value) elif prop_name not in new_props and prop_name in old_props: # Report each REMOVED property delta_value = PropertyDelta( DELTA_TYPE_PROP, object_path, iface_name, prop_name, old_props[prop_name]) yield DeltaRecord(DELTA_DIR_MINUS, delta_value) else: old_value = old_props[prop_name] new_value = new_props[prop_name] if old_value != new_value: # Report each changed property yield DeltaRecord(DELTA_DIR_MINUS, PropertyDelta( DELTA_TYPE_PROP, object_path, iface_name, prop_name, old_value)) yield DeltaRecord(DELTA_DIR_PLUS, PropertyDelta( DELTA_TYPE_PROP, object_path, iface_name, prop_name, new_value)) class UDisks2StorageDeviceListener: """ Implementation of the storage device listener concept for UDisks2 backend. Loosely modeled on the UDisks-based implementation above. Implementation details ^^^^^^^^^^^^^^^^^^^^^^ The class, once configured reacts to asynchronous events from the event loop. Those are either DBus signals or GLib timeout. The timeout, if reached, terminates the test and fails with an appropriate end-user message. The user is expected to manipulate storage devices while the test is running. DBus signals (that correspond to UDisks2 DBus signals) cause callbacks into this code. Each time a signal is reported "delta" is computed and verified to determine if there was a successful match. The delta contains a list or DeltaRecord objects that encode difference (either addition or removal) and the value of the difference (interface name or interface property value). This delta is computed by udisks2_objects_delta(). The delta is then passed to _validate_delta() which has a chance to end the test but also prints diagnostic messages in verbose mode. This is very useful for understanding what the test actually sees occurring. Insertion/removal detection strategy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Compared to initial state, the following changes objects need to be detected * At least one UDisks2 object with the following _all_ interfaces: * UDisks2.Partition (because we want a partitioned device) * UDisks2.Block (because we want that device to have a block device that users can format) - having IdUsage == 'filesystem' (because it should not be a piece of raid or lvm) - having Size > 0 (because it should not be and empty removable storage reader) * UDisks2.Filesystem (because we want to ensure that a filesystem gets mounted) - having MountPoints != [] - as a special exception this rule is REMOVED from eSATA and SATA devices as they are not automatically mounted anymore. This object must be traceable to an UDisks.Drive object: (because we need the medium to be inserted somewhere) - having ConnectionBus in (desired_connection_buses) - as a special exception this rule is weakened for eSATA because for such devices the ConnectionBus property is empty. """ # Name of the DBus interface exposed UDisks2 for various drives UDISKS2_DRIVE_INTERFACE = "org.freedesktop.UDisks2.Drive" # Name of the DBus property provided by the "Drive" interface above UDISKS2_DRIVE_PROPERTY_CONNECTION_BUS = "ConnectionBus" def __init__(self, system_bus, loop, action, devices, minimum_speed, memorycard): # Store the desired minimum speed of the device in Mbit/s. The argument # is passed as the number of bits per second so let's fix that. self._desired_minimum_speed = minimum_speed / 10 ** 6 # Compute the allowed UDisks2.Drive.ConnectionBus value based on the # legacy arguments passed from the command line. self._desired_connection_buses = set([ map_udisks1_connection_bus(device) for device in devices]) # Check if we are explicitly looking for memory cards self._desired_memory_card = memorycard # Store the desired "delta" direction depending on # whether we test for insertion or removal if action == "insert": self._desired_delta_dir = DELTA_DIR_PLUS elif action == "remove": self._desired_delta_dir = DELTA_DIR_MINUS else: raise ValueError("Unsupported action: {}".format(action)) # Store DBus bus object as we need to pass it to UDisks2 observer self._bus = system_bus # Store event loop object self._loop = loop # Setup UDisks2Observer class to track changes published by UDisks2 self._udisks2_observer = UDisks2Observer() # Set the initial value of reference_objects. # The actual value is only set once in check() self._reference_objects = None # As above, just initializing in init for sake of consistency self._is_reference = None # Setup UDisks2Model to know what the current state is. This is needed # when remove events are reported as they don't carry enough state for # the program to work correctly. Since UDisks2Model only applies the # changes _after_ processing the signals from UDisks2Observer we can # reliably check all of the properties of the removed object / device. self._udisks2_model = UDisks2Model(self._udisks2_observer) # Whenever anything changes call our local change handler # This handler always computes the full delta (versus the # reference state) and decides if we have a match or not self._udisks2_model.on_change.connect(self._on_change) # We may need an udev context for checking the speed of USB devices self._udev_client = GUdev.Client() # A snapshot of udev devices, set in check() self._reference_udev_devices = None # Assume the test passes, this is changed when timeout expires or when # an incorrect device gets inserted. self._error = False def _dump_reference_udisks_objects(self): logging.debug("Reference UDisks2 objects:") for udisks2_object in self._reference_objects: logging.debug(" - %s", udisks2_object) def _dump_reference_udev_devices(self): logging.debug("Reference udev devices:") for udev_device in self._reference_udev_devices: interconnect_speed = get_interconnect_speed(udev_device) if interconnect_speed: logging.debug(" - %s (USB %dMBit/s)", udev_device.get_device_file(), interconnect_speed) else: logging.debug(" - %s", udev_device.get_device_file()) def check(self, timeout): """ Run the configured test and return the result The result is False if the test has failed. The timeout, when non-zero, will make the test fail after the specified seconds have elapsed without conclusive result. """ # Setup a timeout if requested if timeout > 0: GObject.timeout_add_seconds(timeout, self._on_timeout_expired) # Connect the observer to the bus. This will start giving us events # (actually when the loop starts later below) self._udisks2_observer.connect_to_bus(self._bus) # Get the reference snapshot of available devices self._reference_objects = copy.deepcopy(self._current_objects) self._dump_reference_udisks_objects() # Mark the current _reference_objects as ... reference, this is sadly # needed by _summarize_changes() as it sees the snapshot _after_ a # change has occurred and cannot determine if the slope of the 'edge' # of the change. It is purely needed for UI in verbose mode self._is_reference = True # A collection of objects that we gladly ignore because we already # reported on them being somehow inappropriate self._ignored_objects = set() # Get the reference snapshot of available udev devices self._reference_udev_devices = get_udev_block_devices( self._udev_client) self._dump_reference_udev_devices() # Start the loop and wait. The loop will exit either when: # 1) A proper device has been detected (either insertion or removal) # 2) A timeout (optional) has expired self._loop.run() # Return the outcome of the test return self._error def _on_timeout_expired(self): """ Internal function called when the timer expires. Basically it's just here to tell the user the test failed or that the user was unable to alter the device during the allowed time. """ print("You have failed to perform the required manipulation in time") # Fail the test when the timeout was reached self._error = True # Stop the loop now self._loop.quit() def _on_change(self): """ Internal method called by UDisks2Model whenever a change had occurred """ # Compute the changes that had occurred since the reference point delta_records = list(self._get_delta_records()) # Display a summary of changes when we are done self._summarize_changes(delta_records) # If the changes are what we wanted stop the loop matching_devices = self._get_matching_devices(delta_records) if matching_devices: print("Expected device manipulation complete: {}".format( ', '.join(matching_devices))) # And call it a day self._loop.quit() def _get_matching_devices(self, delta_records): """ Internal method called that checks if the delta records match the type of device manipulation we were expecting. Only called from _on_change() Returns a set of paths of block devices that matched """ # Results results = set() # Group changes by DBus object path grouped_records = collections.defaultdict(list) for record in delta_records: grouped_records[record.value.object_path].append(record) # Create another snapshot od udev devices so that we don't do it over # and over in the loop below (besides, if we did that then results # could differ each time). current_udev_devices = get_udev_block_devices(self._udev_client) # Iterate over all UDisks2 objects and their delta records for object_path, records_for_object in grouped_records.items(): # Skip objects we already ignored and complained about before if object_path in self._ignored_objects: continue needs = set(('block-fs', 'partition', 'non-empty', 'mounted')) # As a special exception when the ConnectionBus is allowed to be # empty, as is the case with eSATA devices, do not require the # filesystem to be mounted as gvfs may choose not to mount it # automatically. found = set() drive_object_path = None object_block_device = None for record in records_for_object: # Skip changes opposite to the ones we need if record.delta_dir != self._desired_delta_dir: continue # For devices with empty "ConnectionBus" property, don't # require the device to be mounted if (record.value.iface_name == "org.freedesktop.UDisks2.Drive" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "ConnectionBus" and record.value.prop_value == ""): needs.remove('mounted') # Detect block devices designated for filesystems if (record.value.iface_name == "org.freedesktop.UDisks2.Block" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "IdUsage" and record.value.prop_value == "filesystem"): found.add('block-fs') # Memorize the block device path elif (record.value.iface_name == "org.freedesktop.UDisks2.Block" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "PreferredDevice"): object_block_device = record.value.prop_value # Ensure the device is a partition elif (record.value.iface_name == "org.freedesktop.UDisks2.Partition" and record.value.delta_type == DELTA_TYPE_IFACE): found.add('partition') # Ensure the device is not empty elif (record.value.iface_name == "org.freedesktop.UDisks2.Block" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "Size" and record.value.prop_value > 0): found.add('non-empty') # Ensure the filesystem is mounted elif (record.value.iface_name == "org.freedesktop.UDisks2.Filesystem" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "MountPoints" and record.value.prop_value != []): found.add('mounted') # Finally memorize the drive the block device belongs to elif (record.value.iface_name == "org.freedesktop.UDisks2.Block" and record.value.delta_type == DELTA_TYPE_PROP and record.value.prop_name == "Drive"): drive_object_path = record.value.prop_value logging.debug("Finished analyzing %s, found: %s, needs: %s" " drive_object_path: %s", object_path, found, needs, drive_object_path) if needs != found or drive_object_path is None: continue # We've found our candidate, let's look at the drive it belongs # to. We need to do this as some properties are associated with # the drive, not the filesystem/block device and the drive may # not have been inserted at all. try: drive_object = self._current_objects[drive_object_path] except KeyError: # The drive may be removed along with the device, let's check # if we originally saw it try: drive_object = self._reference_objects[drive_object_path] except KeyError: logging.error( "A block device belongs to a drive we could not find") logging.error("missing drive: %r", drive_object_path) continue try: drive_props = drive_object["org.freedesktop.UDisks2.Drive"] except KeyError: logging.error( "A block device belongs to an object that is not a Drive") logging.error("strange object: %r", drive_object_path) continue # Ensure the drive is on the appropriate bus connection_bus = drive_props["ConnectionBus"] if connection_bus not in self._desired_connection_buses: logging.warning("The object %r belongs to drive %r that" " is attached to the bus %r but but we are" " looking for one of %r so it cannot match", object_block_device, drive_object_path, connection_bus, ", ".join(self._desired_connection_buses)) # Ignore this object so that we don't spam the user twice self._ignored_objects.add(object_path) continue # Ensure it is a media card reader if this was explicitly requested drive_is_reader = is_memory_card( drive_props['Vendor'], drive_props['Model'], drive_props['Media']) if self._desired_memory_card and not drive_is_reader: logging.warning( "The object %s belongs to drive %s that does not seem to" " be a media reader", object_block_device, drive_object_path) # Ignore this object so that we don't spam the user twice self._ignored_objects.add(object_path) continue # Ensure the desired minimum speed is enforced if self._desired_minimum_speed: # We need to discover the speed of the UDisks2 object that is # about to be matched. Sadly UDisks2 no longer supports this # property so we need to poke deeper and resort to udev. # # The UDisks2 object that we are interested in implements a # number of interfaces, most notably # org.freedesktop.UDisks2.Block, that has the Device property # holding the unix filesystem path (like /dev/sdb1). We already # hold a reference to that as 'object_block_device' # # We take this as a start and attempt to locate the udev Device # (don't confuse with UDisks2.Device, they are _not_ the same) # that is associated with that path. if self._desired_delta_dir == DELTA_DIR_PLUS: # If we are looking for additions then look at _current_ # collection of udev devices udev_devices = current_udev_devices udisks2_object = self._current_objects[object_path] else: # If we are looking for removals then look at referece # collection of udev devices udev_devices = self._reference_udev_devices udisks2_object = self._reference_objects[object_path] try: # Try to locate the corresponding udev device among the # collection we've selected. Use the drive object as the # key -- this looks for the drive, not partition objects! udev_device = lookup_udev_device(udisks2_object, udev_devices) except LookupError: logging.error("Unable to map UDisks2 object %s to udev", object_block_device) # Ignore this object so that we don't spam the user twice self._ignored_objects.add(object_path) continue interconnect_speed = get_interconnect_speed(udev_device) # Now that we know the speed of the interconnect we can try to # validate it against our desired speed. if interconnect_speed is None: logging.warning("Unable to determine interconnect speed of" " device %s", object_block_device) # Ignore this object so that we don't spam the user twice self._ignored_objects.add(object_path) continue elif interconnect_speed < self._desired_minimum_speed: logging.warning( "Device %s is connected via an interconnect that has" " the speed of %dMbit/s but the required speed was" " %dMbit/s", object_block_device, interconnect_speed, self._desired_minimum_speed) # Ignore this object so that we don't spam the user twice self._ignored_objects.add(object_path) continue else: logging.info("Device %s is connected via an USB" " interconnect with the speed of %dMbit/s", object_block_device, interconnect_speed) # Yay, success results.add(object_block_device) return results @property def _current_objects(self): return self._udisks2_model.managed_objects def _get_delta_records(self): """ Internal method used to compute the delta between reference devices and current devices. The result is a generator of DeltaRecord objects. """ assert self._reference_objects is not None, "Only usable after check()" old = self._reference_objects new = self._current_objects return udisks2_objects_delta(old, new) def _summarize_changes(self, delta_records): """ Internal method used to summarize changes (compared to reference state) called whenever _on_change() gets called. Only visible in verbose mode """ # Filter out anything but interface changes flat_records = [record for record in delta_records if record.value.delta_type == DELTA_TYPE_IFACE] # Group changes by DBus object path grouped_records = collections.defaultdict(list) for record in flat_records: grouped_records[record.value.object_path].append(record) # Bail out quickly when nothing got changed if not flat_records: if not self._is_reference: logging.info("You have returned to the reference state") self._is_reference = True return else: self._is_reference = False # Iterate over grouped delta records for all objects logging.info("Compared to the reference state you have:") for object_path in sorted(grouped_records.keys()): records_for_object = sorted( grouped_records[object_path], key=lambda record: record.value.iface_name) # Skip any job objects as they just add noise if any((record.value.iface_name == "org.freedesktop.UDisks2.Job" for record in records_for_object)): continue logging.info("For object %s", object_path) for record in records_for_object: # Ignore property changes for now if record.value.delta_type != DELTA_TYPE_IFACE: continue # Get the name of the interface that was affected iface_name = record.value.iface_name # Get the properties for that interface (for removals get the # reference values, for additions get the current values) if record.delta_dir == DELTA_DIR_PLUS: props = self._current_objects[object_path][iface_name] action = "inserted" else: props = self._reference_objects[object_path][iface_name] action = "removed" # Display some human-readable information associated with each # interface change if iface_name == "org.freedesktop.UDisks2.Drive": logging.info("\t * %s a drive", action) logging.info("\t vendor and name: %r %r", props['Vendor'], props['Model']) logging.info("\t bus: %s", props['ConnectionBus']) logging.info("\t size: %s", format_bytes(props['Size'])) logging.info("\t is media card: %s", is_memory_card( props['Vendor'], props['Model'], props['Media'])) logging.info("\t current media: %s", props['Media'] or "???" if props['MediaAvailable'] else "N/A") elif iface_name == "org.freedesktop.UDisks2.Block": logging.info("\t * %s block device", action) logging.info("\t from drive: %s", props['Drive']) logging.info("\t having device: %s", props['Device']) logging.info("\t having usage, type and version:" " %s %s %s", props['IdUsage'], props['IdType'], props['IdVersion']) logging.info("\t having label: %s", props['IdLabel']) elif iface_name == "org.freedesktop.UDisks2.PartitionTable": logging.info("\t * %s partition table", action) logging.info("\t having type: %r", props['Type']) elif iface_name == "org.freedesktop.UDisks2.Partition": logging.info("\t * %s partition", action) logging.info("\t from partition table: %s", props['Table']) logging.info("\t having size: %s", format_bytes(props['Size'])) logging.info("\t having name: %r", props['Name']) elif iface_name == "org.freedesktop.UDisks2.Filesystem": logging.info("\t * %s file system", action) logging.info("\t having mount points: %r", props['MountPoints']) def main(): description = "Wait for the specified device to be inserted or removed." parser = argparse.ArgumentParser(description=description) parser.add_argument('action', choices=['insert', 'remove']) parser.add_argument('device', choices=['usb', 'sdio', 'firewire', 'scsi', 'ata_serial_esata'], nargs="+") memorycard_help = ("Memory cards devices on bus other than sdio require " "this parameter to identify them as such") parser.add_argument('--memorycard', action="store_true", help=memorycard_help) parser.add_argument('--timeout', type=int, default=20) min_speed_help = ("Will only accept a device if its connection speed " "attribute is higher than this value " "(in bits/s)") parser.add_argument('--minimum_speed', '-m', help=min_speed_help, type=int, default=0) parser.add_argument('--verbose', action='store_const', const=logging.INFO, dest='logging_level', help="Enable verbose output") parser.add_argument('--debug', action='store_const', const=logging.DEBUG, dest='logging_level', help="Enable debugging") parser.set_defaults(logging_level=logging.WARNING) args = parser.parse_args() # Configure logging as requested # XXX: This may be incorrect as logging.basicConfig() fails after any other # call to logging.log(). The proper solution is to setup a verbose logging # configuration and I didn't want to do it now. logging.basicConfig( level=args.logging_level, format='[%(asctime)s] %(levelname)s:%(name)s:%(message)s') # Connect to the system bus, we also get the event # loop as we need it to start listening for signals. system_bus, loop = connect_to_system_bus() # Check if system bus has the UDisks2 object if is_udisks2_supported(system_bus): # Construct the listener with all of the arguments provided on the # command line and the explicit system_bus, loop objects. logging.debug("Using UDisks2 interface") listener = UDisks2StorageDeviceListener( system_bus, loop, args.action, args.device, args.minimum_speed, args.memorycard) else: # Construct the listener with all of the arguments provided on the # command line and the explicit system_bus, loop objects. logging.debug("Using UDisks1 interface") listener = UDisks1StorageDeviceListener( system_bus, loop, args.action, args.device, args.minimum_speed, args.memorycard) # Run the actual listener and wait till it either times out of discovers # the appropriate media changes try: return listener.check(args.timeout) except KeyboardInterrupt: return 1 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/filter_templates0000775000175000017500000000666412320541306025075 0ustar zygazyga00000000000000#!/usr/bin/env python3 import re import sys import posixpath from optparse import OptionParser from checkbox_support.lib.path import path_expand_recursive from checkbox_support.lib.template import Template class FilterError(Exception): pass def compile_filters(filters): patterns = {} for filter in filters: if "=" not in filter: raise FilterError("Missing assignment in filter: %s" % filter) name, value = filter.split("=", 1) try: pattern = re.compile(r"^%s$" % value) except re.error: raise FilterError("Invalid regular expression in filter: %s" % value) patterns.setdefault(name, []) patterns[name].append(pattern) return patterns def match_patterns(patterns_table, element): matches = [] for key, patterns in patterns_table.items(): if key not in element: matches.append(False) else: value = element[key] for pattern in patterns: matches.append(True if pattern.match(value) else False) return matches def match_elements(elements, attributes=[], whitelist=[], blacklist=[]): whitelist_patterns = compile_filters(whitelist) blacklist_patterns = compile_filters(blacklist) # Apply attributes for element in elements: for attribute in attributes: name, value = attribute.split("=", 1) element[name] = value # Apply whitelist and blacklist matches = [] for element in elements: if whitelist_patterns \ and True not in match_patterns(whitelist_patterns, element): continue if blacklist_patterns \ and True in match_patterns(blacklist_patterns, element): continue matches.append(element) return matches def parse_file(file, *args, **kwargs): template = Template() matches = match_elements(template.load_file(file), *args, **kwargs) template.dump_file(matches, sys.stdout) def parse_path(path, *args, **kwargs): for filename in path_expand_recursive(path): print("# %s" % filename) name = posixpath.basename(filename) if name.startswith(".") or name.endswith("~"): continue file = open(filename, "r") parse_file(file, *args, **kwargs) def parse_paths(paths, *args, **kwargs): for path in paths: parse_path(path, *args, **kwargs) def main(args): usage = "Usage: %prog [OPTIONS] [FILE...]" parser = OptionParser(usage=usage) parser.add_option("-a", "--attribute", action="append", type="string", default=[], help="Set additional attributes by name and value.") parser.add_option("-b", "--blacklist", action="append", type="string", default=[], help="Blacklist of elements by name and value.") parser.add_option("-w", "--whitelist", action="append", type="string", default=[], help="Whitelist of elements by name and value.") (options, args) = parser.parse_args(args) if args: parse_func = parse_paths else: parse_func = parse_file args = sys.stdin try: parse_func(args, options.attribute, options.whitelist, options.blacklist) except FilterError as error: parser.error(error.args[0]) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/mm-test0000775000175000017500000003647112320541306023117 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- Mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details: # # Copyright (C) 2008 Novell, Inc. # Copyright (C) 2009 Red Hat, Inc. # import sys, dbus, time, os, string, subprocess, socket DBUS_INTERFACE_PROPERTIES='org.freedesktop.DBus.Properties' MM_DBUS_SERVICE='org.freedesktop.ModemManager' MM_DBUS_PATH='/org/freedesktop/ModemManager' MM_DBUS_INTERFACE='org.freedesktop.ModemManager' MM_DBUS_INTERFACE_MODEM='org.freedesktop.ModemManager.Modem' MM_DBUS_INTERFACE_MODEM_CDMA='org.freedesktop.ModemManager.Modem.Cdma' MM_DBUS_INTERFACE_MODEM_GSM_CARD='org.freedesktop.ModemManager.Modem.Gsm.Card' MM_DBUS_INTERFACE_MODEM_GSM_NETWORK='org.freedesktop.ModemManager.Modem.Gsm.Network' MM_DBUS_INTERFACE_MODEM_SIMPLE='org.freedesktop.ModemManager.Modem.Simple' def get_cdma_band_class(band_class): if band_class == 1: return "800MHz" elif band_class == 2: return "1900MHz" else: return "Unknown" def get_reg_state(state): if state == 1: return "registered (roaming unknown)" elif state == 2: return "registered on home network" elif state == 3: return "registered on roaming network" else: return "unknown" def cdma_inspect(proxy, dump_private): cdma = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM_CDMA) esn = "" if dump_private: try: esn = cdma.GetEsn() except dbus.exceptions.DBusException: esn = "" print("") print("ESN: %s" % esn) try: (cdma_1x_state, evdo_state) = cdma.GetRegistrationState() print("1x State: %s" % get_reg_state (cdma_1x_state)) print("EVDO State: %s" % get_reg_state (evdo_state)) except dbus.exceptions.DBusException as e: print("Error reading registration state: %s" % e) try: quality = cdma.GetSignalQuality() print("Signal quality: %d" % quality) except dbus.exceptions.DBusException as e: print("Error reading signal quality: %s" % e) try: info = cdma.GetServingSystem() print("Class: %s" % get_cdma_band_class(info[0])) print("Band: %s" % info[1]) print("SID: %d" % info[2]) except dbus.exceptions.DBusException as e: print("Error reading serving system: %s" % e) def cdma_connect(proxy, user, password): # Modem.Simple interface simple = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM_SIMPLE) try: simple.Connect({'number':"#777"}, timeout=92) print("\nConnected!") return True except Exception as e: print("Error connecting: %s" % e) return False def get_gsm_network_mode(modem): mode = modem.GetNetworkMode() if mode == 0x0: mode = "Unknown" elif mode == 0x1: mode = "Any" elif mode == 0x2: mode = "GPRS" elif mode == 0x4: mode = "EDGE" elif mode == 0x8: mode = "UMTS" elif mode == 0x10: mode = "HSDPA" elif mode == 0x20: mode = "2G Preferred" elif mode == 0x40: mode = "3G Preferred" elif mode == 0x80: mode = "2G Only" elif mode == 0x100: mode = "3G Only" elif mode == 0x200: mode = "HSUPA" elif mode == 0x400: mode = "HSPA" else: mode = "(Unknown)" print("Mode: %s" % mode) def get_gsm_band(modem): band = modem.GetBand() if band == 0x0: band = "Unknown" elif band == 0x1: band = "Any" elif band == 0x2: band = "EGSM (900 MHz)" elif band == 0x4: band = "DCS (1800 MHz)" elif band == 0x8: band = "PCS (1900 MHz)" elif band == 0x10: band = "G850 (850 MHz)" elif band == 0x20: band = "U2100 (WCSMA 2100 MHZ, Class I)" elif band == 0x40: band = "U1700 (WCDMA 3GPP UMTS1800 MHz, Class III)" elif band == 0x80: band = "17IV (WCDMA 3GPP AWS 1700/2100 MHz, Class IV)" elif band == 0x100: band = "U800 (WCDMA 3GPP UMTS800 MHz, Class VI)" elif band == 0x200: band = "U850 (WCDMA 3GPP UMT850 MHz, Class V)" elif band == 0x400: band = "U900 (WCDMA 3GPP UMTS900 MHz, Class VIII)" elif band == 0x800: band = "U17IX (WCDMA 3GPP UMTS MHz, Class IX)" else: band = "(invalid)" print("Band: %s" % band) def gsm_inspect(proxy, dump_private, do_scan): # Gsm.Card interface card = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM_GSM_CARD) imei = "" imsi = "" if dump_private: try: imei = card.GetImei() except dbus.exceptions.DBusException: imei = "" try: imsi = card.GetImsi() except dbus.exceptions.DBusException: imsi = "" print("IMEI: %s" % imei) print("IMSI: %s" % imsi) # Gsm.Network interface net = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM_GSM_NETWORK) try: quality = net.GetSignalQuality() print("Signal quality: %d" % quality) except dbus.exceptions.DBusException as e: print("Error reading signal quality: %s" % e) if not do_scan: return print("Scanning...") try: results = net.Scan(timeout=120) except dbus.exceptions.DBusException as e: print("Error scanning: %s" % e) results = {} for r in results: status = r['status'] if status == "1": status = "available" elif status == "2": status = "current" elif status == "3": status = "forbidden" else: status = "(Unknown)" access_tech = "" try: access_tech_num = r['access-tech'] if access_tech_num == "0": access_tech = "(GSM)" elif access_tech_num == "1": access_tech = "(Compact GSM)" elif access_tech_num == "2": access_tech = "(UMTS)" elif access_tech_num == "3": access_tech = "(EDGE)" elif access_tech_num == "4": access_tech = "(HSDPA)" elif access_tech_num == "5": access_tech = "(HSUPA)" elif access_tech_num == "6": access_tech = "(HSPA)" except KeyError: pass if 'operator-long' in r and len(r['operator-long']): print("%s: %s %s" % (r['operator-long'], status, access_tech)) elif 'operator-short' in r and len(r['operator-short']): print("%s: %s %s" % (r['operator-short'], status, access_tech)) else: print("%s: %s %s" % (r['operator-num'], status, access_tech)) def gsm_connect(proxy, apn, user, password): # Modem.Simple interface simple = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM_SIMPLE) try: opts = {'number':"*99#"} if apn is not None: opts['apn'] = apn if user is not None: opts['username'] = user if password is not None: opts['password'] = password simple.Connect(opts, timeout=120) print("\nConnected!") return True except Exception as e: print("Error connecting: %s" % e) return False def pppd_find(): paths = ["/usr/local/sbin/pppd", "/usr/sbin/pppd", "/sbin/pppd"] for p in paths: if os.path.exists(p): return p return None def ppp_start(device, user, password, tmpfile): path = pppd_find() if not path: return None args = [path] args += ["nodetach"] args += ["lock"] args += ["nodefaultroute"] args += ["debug"] if user: args += ["user"] args += [user] args += ["noipdefault"] args += ["115200"] args += ["noauth"] args += ["crtscts"] args += ["modem"] args += ["usepeerdns"] args += ["ipparam"] ipparam = "" if user: ipparam += user ipparam += "+" if password: ipparam += password ipparam += "+" ipparam += tmpfile args += [ipparam] args += ["plugin"] args += ["mm-test-pppd-plugin.so"] args += [device] return subprocess.Popen(args, close_fds=True, cwd="/", env={}) def ppp_wait(p, tmpfile): i = 0 while p.poll() == None and i < 30: time.sleep(1) if os.path.exists(tmpfile): f = open(tmpfile, 'r') stuff = f.read(500) idx = string.find(stuff, "DONE") f.close() if idx >= 0: return True i += 1 return False def ppp_stop(p): import signal p.send_signal(signal.SIGTERM) p.wait() def ntop_helper(ip): ip = socket.ntohl(ip) n1 = ip >> 24 & 0xFF n2 = ip >> 16 & 0xFF n3 = ip >> 8 & 0xFF n4 = ip & 0xFF a = "%c%c%c%c" % (n1, n2, n3, n4) return socket.inet_ntop(socket.AF_INET, a) def static_start(iface, modem): (addr_num, dns1_num, dns2_num, dns3_num) = modem.GetIP4Config() addr = ntop_helper(addr_num) dns1 = ntop_helper(dns1_num) dns2 = ntop_helper(dns2_num) configure_iface(iface, addr, 0, dns1, dns2) def down_iface(iface): ip = ["ip", "addr", "flush", "dev", iface] print(" ".join(ip)) subprocess.call(ip) ip = ["ip", "link", "set", iface, "down"] print(" ".join(ip)) subprocess.call(ip) def configure_iface(iface, addr, gw, dns1, dns2): print("\n\n******************************") print("iface: %s" % iface) print("addr: %s" % addr) print("gw: %s" % gw) print("dns1: %s" % dns1) print("dns2: %s" % dns2) ifconfig = ["ifconfig", iface, "%s/32" % addr] if gw != 0: ifconfig += ["pointopoint", gw] print(" ".join(ifconfig)) print("\n******************************\n") subprocess.call(ifconfig) def file_configure_iface(tmpfile): addr = None gw = None iface = None dns1 = None dns2 = None f = open(tmpfile, 'r') lines = f.readlines() for l in lines: if l.startswith("addr"): addr = l[len("addr"):].strip() if l.startswith("gateway"): gw = l[len("gateway"):].strip() if l.startswith("iface"): iface = l[len("iface"):].strip() if l.startswith("dns1"): dns1 = l[len("dns1"):].strip() if l.startswith("dns2"): dns2 = l[len("dns2"):].strip() f.close() configure_iface(iface, addr, gw, dns1, dns2) return iface def try_ping(iface): cmd = ["ping", "-I", iface, "-c", "4", "-i", "3", "-w", "20", "4.2.2.1"] print(" ".join(cmd)) retcode = subprocess.call(cmd) if retcode != 0: print("PING: failed") else: print("PING: success") dump_private = False connect = False apn = None user = None password = None do_ip = False do_scan = True x = 1 while x < len(sys.argv): if sys.argv[x] == "--private": dump_private = True elif sys.argv[x] == "--connect": connect = True elif (sys.argv[x] == "--user" or sys.argv[x] == "--username"): x += 1 user = sys.argv[x] elif sys.argv[x] == "--apn": x += 1 apn = sys.argv[x] elif sys.argv[x] == "--password": x += 1 password = sys.argv[x] elif sys.argv[x] == "--ip": do_ip = True if os.geteuid() != 0: print("You probably want to be root to use --ip") sys.exit(1) elif sys.argv[x] == "--no-scan": do_scan = False x += 1 bus = dbus.SystemBus() # Get available modems: try: manager_proxy = bus.get_object('org.freedesktop.ModemManager', '/org/freedesktop/ModemManager') manager_iface = dbus.Interface(manager_proxy, dbus_interface='org.freedesktop.ModemManager') modems = manager_iface.EnumerateDevices() except dbus.exceptions.DBusException as excp: if (excp.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown"): print("ERROR: mm-test doesn't work on ModemManager 1.0 and newer: {}".format( excp.get_dbus_message())) else: print("ERROR: {}".format(excp.get_dbus_message())) sys.exit(1) if not modems: print("No modems found") sys.exit(1) for m in modems: connect_success = False data_device = None proxy = bus.get_object(MM_DBUS_SERVICE, m) # Properties props_iface = dbus.Interface(proxy, dbus_interface='org.freedesktop.DBus.Properties') type = props_iface.Get(MM_DBUS_INTERFACE_MODEM, 'Type') if type == 1: print("GSM modem") elif type == 2: print("CDMA modem") else: print("Invalid modem type: %d" % type) print("Driver: '%s'" % (props_iface.Get(MM_DBUS_INTERFACE_MODEM, 'Driver'))) print("Modem device: '%s'" % (props_iface.Get(MM_DBUS_INTERFACE_MODEM, 'MasterDevice'))) data_device = props_iface.Get(MM_DBUS_INTERFACE_MODEM, 'Device') print("Data device: '%s'" % data_device) # Modem interface modem = dbus.Interface(proxy, dbus_interface=MM_DBUS_INTERFACE_MODEM) try: modem.Enable(True) except dbus.exceptions.DBusException as e: print("Error enabling modem: %s" % e) sys.exit(1) info = modem.GetInfo() print("Vendor: %s" % info[0]) print("Model: %s" % info[1]) print("Version: %s" % info[2]) if type == 1: gsm_inspect(proxy, dump_private, do_scan) if connect == True: connect_success = gsm_connect(proxy, apn, user, password) elif type == 2: cdma_inspect(proxy, dump_private) if connect == True: connect_success = cdma_connect(proxy, user, password) print() if connect_success and do_ip: tmpfile = "/tmp/mm-test-%d.tmp" % os.getpid() success = False try: ip_method = props_iface.Get(MM_DBUS_INTERFACE_MODEM, 'IpMethod') if ip_method == 0: # ppp p = ppp_start(data_device, user, password, tmpfile) if ppp_wait(p, tmpfile): data_device = file_configure_iface(tmpfile) success = True elif ip_method == 1: # static static_start(data_device, modem) success = True elif ip_method == 2: # dhcp pass except Exception as e: print("Error setting up IP: %s" % e) if success: try_ping(data_device) print("Waiting for 30s...") time.sleep(30) print("Disconnecting...") try: if ip_method == 0: ppp_stop(p) try: os.remove(tmpfile) except: pass elif ip_method == 1: # static down_iface(data_device) elif ip_method == 2: # dhcp down_iface(data_device) modem.Disconnect() except Exception as e: print("Error tearing down IP: %s" % e) time.sleep(5) modem.Enable(False) 2013.com.canonical.certification.checkbox-0.4/bin/alsa_record_playback0000775000175000017500000000036412320541306025645 0ustar zygazyga00000000000000#!/bin/sh OUTPUT=`mktemp -d` gst_pipeline_test -t 5 "autoaudiosrc ! audioconvert ! level name=recordlevel interval=10000000 ! audioconvert ! wavenc ! filesink location=$OUTPUT/test.wav" aplay $OUTPUT/test.wav rm $OUTPUT/test.wav rmdir $OUTPUT 2013.com.canonical.certification.checkbox-0.4/bin/max_diskspace_used0000775000175000017500000000156012320541306025353 0ustar zygazyga00000000000000#!/bin/bash # Verify default partitioning has used the entire local hard disk. # Please remove any non-local attached storage prior to running this # test. for disk in $@; do echo "Checking maximum disk space available on : $disk" psize=`parted -l | grep $disk | awk '{print $3}'` if [ -n "$psize" ] then echo "Disk space available : $psize" fsizes=`df -B ${psize:(-2)} | grep $disk | awk '{print $2}'` if [ -n "$fsizes" ] then echo "Disk space used : $fsizes" fsize=0 for i in $fsizes; do i=`echo $i | grep -oe '[0-9\.]*'` fsize=$(($fsize + $i)) done psize=`echo $psize | grep -oe '[0-9\.]*'` pct_difference=`awk "BEGIN{print(($psize - $fsize) / $fsize)}"` echo "Difference ( > 0.15 fails ) : $pct_difference" awk "BEGIN{exit($pct_difference > 0.15)}" || exit 1 fi fi done 2013.com.canonical.certification.checkbox-0.4/bin/storage_test0000775000175000017500000000475212320541306024231 0ustar zygazyga00000000000000#!/bin/bash # take the path of the storage device and test is it a block device. function run_bonnie() { echo "Running bonnie++ on $1..." mount_point=$(df -h | grep -m 1 $1 | awk '{print $6}') echo "Putting scratch disk at $mount_point" mkdir -p "$mount_point/tmp/scratchdir" # When running on disks with small drives (SSD/flash) we need to do # some tweaking. Bonnie uses 2x RAM by default to write data. If that's # more than available disk space, the test will fail inappropriately. free_space=$(df -m | grep -m 1 $1 | awk '{print $4}') echo " Disk $1 has ${free_space}MB available" memory=$(free -m |grep Mem|awk '{print $2}') echo " System has ${memory}MB RAM" disk_needed=$((memory * 2)) echo " We need ${disk_needed}MB of disk space for testing" if [[ "$disk_needed" -ge "$free_space" ]]; then echo " Insufficient disk space available for defaults." echo " reducing memory footprint to be 1/2 of free disk space." # we need to pass an amount that's 1/2 the amount of available disk # space to bonnie++ so she doesn't overwrite and fail. disk_needed=$(($free_space/2)) bonnie++ -d $mount_point/tmp/scratchdir -u root -r $disk_needed else echo " Free disk space is sufficient to continue testing." bonnie++ -d $mount_point/tmp/scratchdir -u root fi } disk=/dev/$1 if [ -b $disk ] then echo "$disk is a block device" size=`parted -l -s | grep $disk | awk '{print $3}'` if [ -n "$size" ] then echo "$disk reports a size of $size." # Have to account for the end of the size descriptor size_range=${size:(-2)} if mount | grep -q $disk then echo "$disk is mounted, proceeding." else echo "$disk is not mounted. It must be mounted before testing." exit 1 fi if [ $size_range == "KB" ] then echo "$disk is too small to be functioning." exit 1 elif [ $size_range == "MB" ] then size_int=${size::${#size}-2} if [ $size_int -gt 10 ] then run_bonnie $disk else echo "$disk is too small to be functioning." exit 1 fi else run_bonnie $disk fi else echo "$disk doesn't report a size." exit 1 fi else echo "$disk is not listed as a block device." exit 1 fi 2013.com.canonical.certification.checkbox-0.4/bin/rotation_test0000775000175000017500000000416612320541306024423 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # rotation_test # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import time import sys from checkbox_support.contrib import xrandr def rotate_screen(rotation): # Refresh the screen. Required by NVIDIA screen = xrandr.get_current_screen() screen.set_rotation(rotation) return screen.apply_config() def main(): screen = xrandr.get_current_screen() rotations = {'normal': xrandr.RR_ROTATE_0, 'right': xrandr.RR_ROTATE_90, 'inverted': xrandr.RR_ROTATE_180, 'left': xrandr.RR_ROTATE_270} rots_statuses = {} for rot in rotations: try: status = rotate_screen(rotations[rot]) except(xrandr.RRError, xrandr.UnsupportedRRError) as error: status = 1 else: error = 'N/A' # Collect the status and the error message rots_statuses[rot] = (status, error) time.sleep(4) # Try to set the screen back to normal try: rotate_screen(xrandr.RR_ROTATE_0) except(xrandr.RRError, xrandr.UnsupportedRRError) as error: print(error) result = 0 for elem in rots_statuses: status = rots_statuses.get(elem)[0] error = rots_statuses.get(elem)[1] if status != 0: print('Error: rotation "%s" failed with status %d: %s.' % (elem, status, error), file=sys.stderr) result = 1 return result if __name__ == '__main__': exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/fwts_test0000775000175000017500000003451612320565735023565 0ustar zygazyga00000000000000#! /usr/bin/python3 import sys import re from time import time, sleep from argparse import ArgumentParser, RawTextHelpFormatter, REMAINDER from subprocess import Popen, PIPE from syslog import * TESTS = ['acpidump', 'acpiinfo', 'acpitables', 'apicedge', 'apicinstance', 'aspm', 'bios32', 'bios_info', 'checksum', 'cstates', 'dmar', 'ebda', 'fadt', 'hpet_check', 'klog', 'mcfg', 'method', 'mpcheck', 'msr', 'mtrr', 'nx', 'oops', 'uefirtvariable', 'version', 'virt', 'wmi'] def get_sleep_times(start_marker, end_marker, sleep_time, resume_time): logfile = '/var/log/syslog' log_fh = open(logfile, 'r') line = '' run = 'FAIL' sleep_start_time = 0.0 sleep_end_time = 0.0 resume_start_time = 0.0 resume_end_time = 0.0 while start_marker not in line: try: line = log_fh.readline() except UnicodeDecodeError: continue if start_marker in line: loglist = log_fh.readlines() for idx in range(0, len(loglist)): if 'PM: Syncing filesystems' in loglist[idx]: sleep_start_time = re.split('[\[\]]', loglist[idx])[1].strip() if 'ACPI: Low-level resume complete' in loglist[idx]: sleep_end_time = re.split('[\[\]]', loglist[idx - 1])[1].strip() resume_start_time = re.split('[\[\]]', loglist[idx])[1].strip() idx += 1 if 'Restarting tasks' in loglist[idx]: resume_end_time = re.split('[\[\]]', loglist[idx])[1].strip() if end_marker in loglist[idx]: run = 'PASS' break sleep_elapsed = float(sleep_end_time) - float(sleep_start_time) resume_elapsed = float(resume_end_time) - float(resume_start_time) return (run, sleep_elapsed, resume_elapsed) def average_times(runs): sleep_total = 0.0 resume_total = 0.0 run_count = 0 for run in runs.keys(): run_count += 1 sleep_total += runs[run][1] resume_total += runs[run][2] sleep_avg = sleep_total / run_count resume_avg = resume_total / run_count print('Average time to sleep: %0.5f' % sleep_avg) print('Average time to resume: %0.5f' % resume_avg) def fix_sleep_args(args): new_args = [] for arg in args: if "=" in arg: new_args.extend(arg.split('=')) else: new_args.append(arg) return new_args def main(): description_text = 'Tests the system BIOS using the Firmware Test Suite' epilog_text = ('To perform sleep testing, you will need at least some of ' 'the following options: \n' 's3 or s4: tells fwts which type of sleep to perform.\n' '--s3-delay-delta\n' '--s3-device-check\n' '--s3-device-check-delay\n' '--s3-max-delay\n' '--s3-min-delay\n' '--s3-multiple\n' '--s3-quirks\n' '--s3-sleep-delay\n' '--s3power-sleep-delay\n\n' 'Example: fwts-test --sleep s3 --s3-min-delay 30 ' '--s3-multiple 10 --s3-device-check\n\n' 'For further help with sleep options:\n' 'fwts-test --fwts-help') parser = ArgumentParser(description=description_text, epilog=epilog_text, formatter_class=RawTextHelpFormatter) parser.add_argument('-l', '--log', default='/tmp/fwts_results.log', help=('Specify the location and name of the log file.\n' '[Default: %(default)s]')) parser.add_argument('-f', '--fail-level', default='high', choices=['critical', 'high', 'medium', 'low', 'none', 'aborted'], help=('Specify the FWTS failure level that will trigger ' 'this script to return a failing exit code. For ' 'example, if you chose "critical" as the ' 'fail-level, this wrapper will NOT return a ' 'failing exit code unless FWTS reports a test as ' 'FAILED_CRITICAL. You will still be notified of ' 'all FWTS test failures. [Default level: ' '%(default)s]')) sleep_args = parser.add_argument_group('Sleep Options', ('The following arguments are to only be used with the ' '--sleep test option')) sleep_args.add_argument('--sleep-time', dest='sleep_time', action='store', help=('The max time in seconds that a system ' 'should take\nto completely enter sleep. ' 'Anything more than this\ntime will cause ' 'that test iteration to fail.\n' '[Default: 10s]')) sleep_args.add_argument('--resume-time', dest='resume_time', action='store', help=('Same as --sleep-time, except this applies ' 'to the\ntime it takes a system to fully ' 'wake from sleep.\n[Default: 3s]')) group = parser.add_mutually_exclusive_group() group.add_argument('-t', '--test', action='append', help='Name of the test to run.') group.add_argument('-a', '--all', action='store_true', help='Run ALL FWTS automated tests (assumes -w and -c)') group.add_argument('-s', '--sleep', nargs=REMAINDER, action='store', help=('Perform sleep test(s) using the additional\n' 'arguments provided after --sleep. All remaining\n' 'items on the command line will be passed \n' 'through to fwts for performing sleep tests. \n' 'For info on these extra fwts options, please \n' 'see the epilog below and \n' 'the --fwts-help option.')) group.add_argument('--fwts-help', dest='fwts_help', action='store_true', help='Display the help info for fwts itself (lengthy)') group.add_argument('--list', action='store_true', help='List all tests in fwts.') args = parser.parse_args() tests = [] results = {} critical_fails = [] high_fails = [] medium_fails = [] low_fails = [] passed = [] aborted = [] # Set correct fail level if args.fail_level is not 'none': args.fail_level = 'FAILED_%s' % args.fail_level.upper() # Get our failure priority and create the priority values fail_levels = {'FAILED_CRITICAL':4, 'FAILED_HIGH':3, 'FAILED_MEDIUM':2, 'FAILED_LOW':1, 'FAILED_NONE':0, 'FAILED_ABORTED':-1} fail_priority = fail_levels[args.fail_level] # Enforce only using sleep opts with --sleep if args.sleep_time or args.resume_time and not args.sleep: parser.error('--sleep-time and --resume-time only apply to the ' '--sleep testing option.') if args.fwts_help: Popen('fwts -h', shell=True).communicate()[0] return 0 elif args.list: print('\n'.join(TESTS)) return 0 elif args.test: tests.extend(args.test) elif args.all: tests.extend(['wakealarm', 'cpufreq', 'maxfreq'] + TESTS) elif args.sleep: args.sleep = fix_sleep_args(args.sleep) iterations = 1 # if multiple iterations are requested, we need to intercept # that argument and keep it from being presented to fwts since # we're handling the iterations directly. s3 = '--s3-multiple' s4 = '--s4-multiple' if s3 in args.sleep: iterations = int(args.sleep.pop(args.sleep.index(s3) + 1)) args.sleep.remove(s3) if s4 in args.sleep: iterations = int(args.sleep.pop(args.sleep.index(s4) + 1)) args.sleep.remove(s4) # if we've passed our custom sleep arguments for resume or sleep # time, we need to intercept those as well. resume_time_arg = '--resume-time' sleep_time_arg = '--sleep-time' if resume_time_arg in args.sleep: args.resume_time = int(args.sleep.pop( args.sleep.index(resume_time_arg) + 1)) args.sleep.remove(resume_time_arg) if sleep_time_arg in args.sleep: args.sleep_time = int(args.sleep.pop( args.sleep.index(sleep_time_arg) + 1)) args.sleep.remove(sleep_time_arg) # if we still haven't set a sleep or resume time, use defauts. if not args.sleep_time: args.sleep_time = 10 if not args.resume_time: args.resume_time = 3 tests.extend(args.sleep) else: tests.extend(TESTS) # run the tests we want if args.sleep: iteration_results = {} print('=' * 20 + ' Test Results ' + '=' * 20) for iteration in range(0, iterations): timestamp = int(time()) start_marker = 'CHECKBOX SLEEP TEST START %s' % timestamp end_marker = 'CHECKBOX SLEEP TEST STOP %s' % timestamp syslog(LOG_INFO, '---' + start_marker + '---' + str(time())) command = ('fwts -q --stdout-summary -r %s %s' % (args.log, ' '.join(tests))) results['sleep'] = (Popen(command, stdout=PIPE, shell=True) .communicate()[0].strip()).decode() syslog(LOG_INFO, '---' + end_marker + '---' + str(time())) if 's4' not in args.sleep: iteration_results[iteration] = get_sleep_times(start_marker, end_marker, args.sleep_time, args.resume_time) print(' - Cycle %s: Status: %s Sleep Elapsed: %0.5f ' 'Resume Elapsed: %0.5f' % (iteration, iteration_results[iteration][0], iteration_results[iteration][1], iteration_results[iteration][2])) if 's4' not in args.sleep: average_times(iteration_results) for run in iteration_results.keys(): if 'FAIL' in iteration_results[run]: results['sleep'] = 'FAILED_CRITICAL' else: for test in tests: command = ('fwts -q --stdout-summary -r %s %s' % (args.log, test)) results[test] = (Popen(command, stdout=PIPE, shell=True) .communicate()[0].strip()).decode() # parse the summaries for test in results.keys(): if results[test] == 'FAILED_CRITICAL': critical_fails.append(test) elif results[test] == 'FAILED_HIGH': high_fails.append(test) elif results[test] == 'FAILED_MEDIUM': medium_fails.append(test) elif results[test] == 'FAILED_LOW': low_fails.append(test) elif results[test] == 'PASSED': passed.append(test) elif results[test] == 'ABORTED': aborted.append(test) else: continue if critical_fails: print("Critical Failures: %d" % len(critical_fails)) print("WARNING: The following test cases were reported as critical\n" "level failures by fwts. Please review the log at\n" "%s for more information." % args.log) for test in critical_fails: print(" - " + test) if high_fails: print("High Failures: %d" % len(high_fails)) print("WARNING: The following test cases were reported as high\n" "level failures by fwts. Please review the log at\n" "%s for more information." % args.log) for test in high_fails: print(" - " + test) if medium_fails: print("Medium Failures: %d" % len(medium_fails)) print("WARNING: The following test cases were reported as medium\n" "level failures by fwts. Please review the log at\n" "%s for more information." % args.log) for test in medium_fails: print(" - " + test) if low_fails: print("Low Failures: %d" % len(low_fails)) print("WARNING: The following test cases were reported as low\n" "level failures by fwts. Please review the log at\n" "%s for more information." % args.log) for test in low_fails: print(" - " + test) if passed: print("Passed: %d" % len(passed)) for test in passed: print(" - " + test) if aborted: print("Aborted Tests: %d" % len(aborted)) print("WARNING: The following test cases were aborted by fwts\n" "Please review the log at %s for more information." % args.log) for test in aborted: print(" - " + test) if args.fail_level is not 'none': if fail_priority == fail_levels['FAILED_CRITICAL']: if critical_fails: return 1 if fail_priority == fail_levels['FAILED_HIGH']: if critical_fails or high_fails: return 1 if fail_priority == fail_levels['FAILED_MEDIUM']: if critical_fails or high_fails or medium_fails: return 1 if fail_priority == fail_levels['FAILED_LOW']: if critical_fails or high_fails or medium_fails or low_fails: return 1 if fail_priority == fail_levels['FAILED_ABORTED']: if aborted or critical_fails or high_fails: return 1 return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/audio_driver_info0000775000175000017500000000653712320541306025220 0ustar zygazyga00000000000000#!/usr/bin/python3 import sys import re from argparse import ArgumentParser from subprocess import check_output, STDOUT, CalledProcessError from checkbox_support.parsers.modinfo import ModinfoParser TYPES = ("source", "sink") entries_regex = re.compile("index.*?(?=device.icon_name)", re.DOTALL) driver_regex = re.compile("(?<=driver_name = )\"(.*)\"") name_regex = re.compile("(?<=name:).*") class PacmdAudioDevice(): """ Class representing an audio device with information gathered from pacmd """ def __init__(self, name, driver): self._name = name self._driver = driver self._modinfo = self._modinfo_parser(driver) self._driver_version = self._find_driver_ver() def __str__(self): retstr = "Device: %s\n" % self._name if self._driver: retstr += "Driver: %s (%s)" % (self._driver, self._driver_version) else: retstr += "Driver: Unknown" return retstr def _modinfo_parser(self, driver): cmd = ['/sbin/modinfo', driver] try: stream = check_output(cmd, stderr=STDOUT, universal_newlines=True) except CalledProcessError as err: print("Error running %s:" % cmd, file=sys.stderr) print(err.output, file=sys.stderr) return None if not stream: print("Error: modinfo returned nothing", file=sys.stderr) return None else: parser = ModinfoParser(stream) modinfo = parser.get_all() return modinfo def _find_driver_ver(self): # try the version field first, then vermagic second, some audio # drivers don't report version if the driver is in-tree if self._modinfo['version']: return self._modinfo['version'] else: # vermagic will look like this (below) and we only care about the # first part: # "3.2.0-29-generic SMP mod_unload modversions" return self._modinfo['vermagic'].split()[0] def list_device_info(): """ Lists information on audio devices including device driver and version """ retval = 0 for vtype in TYPES: try: pacmd_entries = check_output(["pacmd", "list-%ss" % vtype], universal_newlines=True) except Exception as e: print("Error when running pacmd list-%ss: %s" % (vtype, e), file=sys.stderr) return 1 entries = entries_regex.findall(pacmd_entries) for entry in entries: name_match = name_regex.search(entry) if name_match: name = name_match.group().strip() else: print("Unable to determine device bus information from the" " pacmd list-%ss output\npacmd output was: %s" % (vtype, pacmd_entries), file=sys.stderr) return 1 driver_name = driver_regex.findall(entry) if driver_name: driver = driver_name[0] else: driver = None print("%s\n" % PacmdAudioDevice(name, driver)) return retval def main(): parser = ArgumentParser("List audio device and driver information") args = parser.parse_args() return list_device_info() if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/xen_test0000775000175000017500000000414112320541306023347 0ustar zygazyga00000000000000#!/bin/bash ## Xen Testing ## Script to make and fire off some pre-made VMs to test the hypervisor ## under a bit of simulated load ## ## USAGE: xentest.sh /path/to/ORIGINAL_IMAGE /path/to/ORIGINAL_CONFIG ## ## this is a kludge and a dirty, dirty hack and shoud die an ignomious ## death soon. A more elegant solution would be to modify the script ## /usr/share/checkbox/scripts/virtualization and add the missing Xen ## functionality. But this will do as a first pass. ORIGINAL_VM=$1 ORIGINAL_VM_TEMPLATE=$2 VIRSH_CMD='virsh -c xen:///' CLONE_CMD='virt-clone' VM_NAME_BASE='xentest-vm' # First, figure out how many CPUs we have: #CPU_CORES=`xm dmesg | grep -c "(XEN) Processor #"` CPU_CORES=1 # Verify our image and config file are present if [ ! -e $ORIGINAL_VM ]; then echo "Xen VM Image not found!" >&2 exit 1 fi if [ ! -e $ORIGINAL_VM_TEMPLATE ]; then echo "Xen VM Config File not found!" >&2 exit 1 fi #Clone those suckers enough that we have 2 VMs per core and LAUNCH! VM_TOTAL=$((CPU_CORES*2)) #Set up an assoticative array (this would translate much better into #a simple python list later on, hint hint) declare -A VM_NAMES echo "Starting $VM_TOTAL VM clones" >&2 for vm in `seq 1 $VM_TOTAL`; do VM_NAME="$VM_NAME_BASE$vm" VM_NAMES[$vm]=$VM_NAME echo "Cloning vm $vm" >&2 $CLONE_CMD --original-xml=$ORIGINAL_VM_TEMPLATE -n $VM_NAME -f /vms/$VM_NAME.img --force >&2 echo "Starting vm $vm" >&2 $VIRSH_CMD start $VM_NAME done #Lets wait a few minutes to let them do some work echo "Sleeping for 5 miunutes to let VMs boot and start working" >&2 sleep 5m echo "" >&2 #Now verify the VMs are still running fail=false echo "Checking domU state..." >&2 for vm in `seq 1 $VM_TOTAL`; do state=`$VIRSH_CMD domstate ${VM_NAMES[$vm]}` echo "${VM_NAMES[$vm]}: $state" >&2 if [ "$state" != "running" ]; then fail=true echo "VM $vm is not in running state!" >&2 fi done if $fail; then echo "One or more guests is not in the running state after 5 minutes." >&2 echo "Test Fails" >&2 exit 1 else echo "All guests seem to be running correctly" fi exit 0 2013.com.canonical.certification.checkbox-0.4/bin/graphics_modes_info0000775000175000017500000000514312320541306025523 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # graphics_modes_info # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . from __future__ import print_function from __future__ import unicode_literals import sys from checkbox_support.contrib import xrandr def print_modes_info(screen): """Print some information about the detected screen and its outputs""" xrandr._check_required_version((1, 0)) print("Screen %s: minimum %s x %s, current %s x %s, maximum %s x %s" %\ (screen._screen, screen._width_min, screen._height_min, screen._width, screen._height, screen._width_max, screen._height_max)) print(" %smm x %smm" % (screen._width_mm, screen._height_mm)) print("Outputs:") for o in list(screen.outputs.keys()): output = screen.outputs[o] print(" %s" % o, end=' ') if output.is_connected(): print("(%smm x %smm)" % (output.get_physical_width(), output.get_physical_height())) modes = output.get_available_modes() print(" Modes:") for m in range(len(modes)): mode = modes[m] refresh = mode.dotClock / (mode.hTotal * mode.vTotal) print(" [%s] %s x %s @ %s Hz" % (m, mode.width, mode.height, refresh), end=' ') if mode.id == output._mode: print("(current)", end=' ') if m == output.get_preferred_mode(): print("(preferred)", end=' ') print("") else: print("(not connected)") def main(): screen = xrandr.get_current_screen() try: print_modes_info(screen) except(xrandr.UnsupportedRRError): print('Error: RandR version lower than 1.0', file=sys.stderr) if __name__ == '__main__': main() 2013.com.canonical.certification.checkbox-0.4/bin/floppy_test0000775000175000017500000000772112320541306024075 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import filecmp import subprocess import posixpath DEFAULT_DIR = "/tmp/checkbox.floppy" DEFAULT_DEVICE_DIR = "floppy_device" DEFAULT_IMAGE_DIR = "floppy_image" DEFAULT_IMAGE = "floppy.img" class FloppyTest(object): def __init__(self, device): self.device = device self.device_dir = os.path.join(DEFAULT_DIR, DEFAULT_DEVICE_DIR) self.image_dir = os.path.join(DEFAULT_DIR, DEFAULT_IMAGE_DIR) self.image = os.path.join(DEFAULT_DIR, DEFAULT_IMAGE) self.interactive = True for dir in (self.device_dir, self.image_dir): if not posixpath.exists(dir): os.makedirs(dir) def run(self): floppyDevice = self.device if floppyDevice: print(" Testing on floppy drive %s " % floppyDevice) else: print(" Error ! No floppy drive found !") return 1 # remove temp files if they exist os.system("umount /media/floppy 2>/dev/null") if (os.path.exists(self.device_dir) or os.path.exists(self.image_dir) or os.path.exists(self.image)): os.system("umount %s %s 2>/dev/null" % (self.device_dir, self.image_dir)) os.system("rm -rf %s %s %s 2>/dev/null" % (self.device_dir, self.image_dir, self.image)) # Create the test images os.mkdir(self.device_dir) os.mkdir(self.image_dir) os.system("dd if=/dev/zero of=%s bs=1k count=1440" % self.image) os.system("mkdosfs %s" % self.image) os.system("mount -o loop %s %s" % (self.image, self.image_dir)) os.system("cp -a /etc/*.conf %s 2> /dev/null" % self.image_dir) os.system("umount %s" % self.image_dir) # start testing (noFloppyDisk, junkOutput1) = \ subprocess.getstatusoutput("dd bs=1c if=%s count=0 2>/dev/null" % floppyDevice) if noFloppyDisk != 0: print("Error ! No floppy disc or bad media in %s !" % floppyDevice) return 1 else: # writing files print(" Writing data to floppy disc ... ") (ddStatus, ddOutput) = \ subprocess.getstatusoutput("dd if=%s of=%s bs=1k count=1440" % (self.image, floppyDevice)) if ddStatus == 0: print(" Write data to floppy disc done ! ") else: print(" Error ! Write data to floppy disc error ! ") print(" Please check if your floppy disc is write-protected !") return 1 # comparing files os.system("mount %s %s" % (floppyDevice, self.device_dir)) os.system("mount -o loop %s %s" % (self.image, self.image_dir)) print(" Comparing files ... ") fileList = os.listdir(self.image_dir) returnValue = 0 for textFile in fileList: file1 = os.path.join(self.device_dir, textFile) file2 = os.path.join(self.image_dir, textFile) if filecmp.cmp(file1, file2): print(" comparing file %s" % textFile) else: print(" -- Error ! File %s comparison failed ! -- " % textFile) returnValue = 1 print(" File comparison done ! ") # remove temp files os.system("umount /media/floppy 2>/dev/null") os.system("umount %s %s " % (self.image_dir, self.device_dir)) os.system("rm -rf %s %s %s" % (self.device_dir, self.image_dir, self.image)) print("Done !") return returnValue def main(args): return_values = [] for device in args: test = FloppyTest(device) return_values.append(test.run()) return 1 in return_values if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/cycle_vts0000775000175000017500000000077112320541306023516 0ustar zygazyga00000000000000#!/bin/bash set -o errexit # NB: This script must be run with root privileges in order to have any effect! CURRENT_VT=`/bin/fgconsole` if [ "$CURRENT_VT" == "" ] then echo "Unable to determine current virtual terminal." >&2 exit 1 fi if [ "$CURRENT_VT" -ne "1" ] then chvt 1 else chvt 2 fi sleep 2 chvt "$CURRENT_VT" sleep 2 # make sure we switched back END_VT=`/bin/fgconsole` if [ "$END_VT" -ne "$CURRENT_VT" ] then echo "didn't get back to the original VT" >&2 exit 1 fi 2013.com.canonical.certification.checkbox-0.4/bin/run_templates0000775000175000017500000000673312320541306024411 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import re import sys import uuid from optparse import OptionParser from subprocess import Popen, PIPE from checkbox_support.lib.template import Template DEFAULT_INPUT = "-" DEFAULT_OUTPUT = "-" COMMAND_TEMPLATE = """cat <<%(separator)s %(input)s %(separator)s""" class Runner(object): def __init__(self, input, output): self.input = input self.output = output def get_args(self, record): return [] def get_env(self, record): env = dict(os.environ) env["NF"] = str(len(record)) return env def process(self, args, shell=False): process = Popen( args, shell=shell, stdout=PIPE, universal_newlines=True) records = self.process_output(process.stdout) for nr, record in enumerate(records): args = self.get_args(record) env = self.get_env(record) env["NR"] = str(nr) command_string = COMMAND_TEMPLATE % { "input": self.input, "separator": uuid.uuid4()} command = ["sh", "-c", command_string] + args process = Popen(command, env=env, stdout=self.output, universal_newlines=True) process.communicate() def process_output(self, output): raise NotImplementedError class LineRunner(Runner): field_separator = r"\s+" record_separator = r"(?:\r?\n)" def get_args(self, record): args = [record] args.extend(re.split(self.field_separator, record)) return args def process_output(self, file): # Strip trailing separator data = re.sub(r"%s$" % self.record_separator, "", file.read()) return re.split(self.record_separator, data) class TemplateRunner(Runner): def get_env(self, record): env = super(TemplateRunner, self).get_env(record) env.update(record) return env def process_output(self, output): template = Template() return template.load_file(output) def main(args): usage = "Usage: %prog [OPTIONS] [COMMAND]" parser = OptionParser(usage=usage) parser.add_option("-i", "--input", metavar="FILE", default=DEFAULT_INPUT, help="Input from the given file name, - for stdin") parser.add_option("-o", "--output", metavar="FILE", default=DEFAULT_OUTPUT, help="Output to the given file name, - for stdout") parser.add_option("-s", "--shell", action="store_true", help="Run the command as a shell script") parser.add_option("-t", "--template", action="store_true", help="Interpret the command output as a template") (options, args) = parser.parse_args(args) # Default args to echo command if not args: args = ["echo"] # Read input if options.input == "-": input = sys.stdin.read() else: input_file = open(options.input, "r") try: input = input_file.read() finally: input_file.close() # Open output if options.output == "-": output_file = sys.stdout else: output_file = open(options.output, "w") # Determine runner class if options.template: runner_class = TemplateRunner else: runner_class = LineRunner runner = runner_class(input, output_file) runner.process(args, options.shell) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/graphics_stress_test0000775000175000017500000003674412320541306025776 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # graphics_stress_test # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import errno import logging import os import re import sys import tempfile import time from argparse import ArgumentParser from subprocess import call, Popen, PIPE from checkbox_support.contrib import xrandr class VtWrapper(object): """docstring for VtWrapper""" def __init__(self): self.x_vt = self._get_x_vt() def _get_x_vt(self): '''Get the vt where X lives''' vt = 0 proc = Popen(['ps', 'aux'], stdout=PIPE, universal_newlines=True) proc_output = proc.communicate()[0].split('\n') proc_line = re.compile('.*tty(\d+).+/usr/bin/X.*') for line in proc_output: match = proc_line.match(line) if match: vt = match.group(1).strip().lower() return int(vt) def set_vt(self, vt): retcode = call(['chvt', '%d' % vt]) return retcode class SuspendWrapper(object): def __init__(self): pass def can_we_sleep(self, mode): ''' Test to see if S3 state is available to us. /proc/acpi/* is old and will be deprecated, using /sys/power to maintine usefulness for future kernels. ''' states_fh = open('/sys/power/state', 'r') try: states = states_fh.read().split() finally: states_fh.close() if mode in states: return True else: return False def get_current_time(self): cur_time = 0 time_fh = open('/sys/class/rtc/rtc0/since_epoch', 'r') try: cur_time = int(time_fh.read()) finally: time_fh.close() return cur_time def set_wake_time(self, time): ''' Get the current epoch time from /sys/class/rtc/rtc0/since_epoch then add time and write our new wake_alarm time to /sys/class/rtc/rtc0/wakealarm. The math could probably be done better but this method avoids having to worry about whether or not we're using UTC or local time for both the hardware and system clocks. ''' cur_time = self.get_current_time() logging.debug('Current epoch time: %s' % cur_time) wakealarm_fh = open('/sys/class/rtc/rtc0/wakealarm', 'w') try: wakealarm_fh.write('0\n') wakealarm_fh.flush() wakealarm_fh.write('+%s\n' % time) wakealarm_fh.flush() finally: wakealarm_fh.close() logging.debug('Wake alarm in %s seconds' % time) def do_suspend(self, mode): ''' Suspend the system and hope it wakes up. Previously tried writing new state to /sys/power/state but that seems to put the system into an uncrecoverable S3 state. So far, pm-suspend seems to be the most reliable way to go. ''' if mode == 'mem': status = call('/usr/sbin/pm-suspend') elif mode == 'disk': status = call('/usr/sbin/pm-hibernate') else: logging.debug('Unknown sleep state passed') status == 1 return status class RotationWrapper(object): def __init__(self): self._rotations = {'normal': xrandr.RR_ROTATE_0, 'right': xrandr.RR_ROTATE_90, 'inverted': xrandr.RR_ROTATE_180, 'left': xrandr.RR_ROTATE_270} def _rotate_screen(self, rotation): # Refresh the screen. Required by NVIDIA screen = xrandr.get_current_screen() screen.set_rotation(rotation) return screen.apply_config() def do_rotation_cycle(self): '''Cycle through all possible rotations''' rots_statuses = {} for rot in self._rotations: try: status = self._rotate_screen(self._rotations[rot]) except(xrandr.RRError, xrandr.UnsupportedRRError) as err: status = 1 error = err else: error = 'N/A' # Collect the status and the error message rots_statuses[rot] = (status, error) time.sleep(4) # Try to set the screen back to normal try: self._rotate_screen(xrandr.RR_ROTATE_0) except(xrandr.RRError, xrandr.UnsupportedRRError) as error: print(error) result = 0 for elem in rots_statuses: status = rots_statuses.get(elem)[0] error = rots_statuses.get(elem)[1] if status != 0: logging.error('Error: rotation "%s" failed with status %d: %s.' % (elem, status, error)) result = 1 return result class RenderCheckWrapper(object): """A simple class to run the rendercheck suites""" def __init__(self, temp_dir=None): self._temp_dir = temp_dir def _print_test_info(self, suites='all', iteration=1, show_errors=False): '''Print the output of the test suite''' main_command = 'rendercheck' passed = 0 total = 0 if self._temp_dir: # Use the specified path temp_file = tempfile.NamedTemporaryFile(dir=self._temp_dir, delete=False) else: # Use /tmp temp_file = tempfile.NamedTemporaryFile(delete=False) if suites == all: full_command = [main_command, '-f', 'a8r8g8b8'] else: full_command = [main_command, '-t', suites, '-f', 'a8r8g8b8'] try: # Let's dump the output into file as it can be very large # and we don't want to store it in memory process = Popen(full_command, stdout=temp_file, universal_newlines=True) except OSError as exc: if exc.errno == errno.ENOENT: logging.error('Error: please make sure that rendercheck ' 'is installed.') exit(1) else: raise exit_code = process.wait() temp_file.close() # Read values from the file errors = re.compile('.*test error.*') results = re.compile('(.+) tests passed of (.+) total.*') first_error = True with open(temp_file.name) as temp_handle: for line in temp_handle: match_output = results.match(line) match_errors = errors.match(line) if match_output: passed = int(match_output.group(1).strip()) total = int(match_output.group(2).strip()) logging.info('Results:') logging.info(' %d tests passed out of %d.' % (passed, total)) if show_errors and match_errors: error = match_errors.group(0).strip() if first_error: logging.debug('Rendercheck %s suite errors ' 'from iteration %d:' % (suites, iteration)) first_error = False logging.debug(' %s' % error) # Remove the file os.unlink(temp_file.name) return (exit_code, passed, total) def run_test(self, suites=[], iterations=1, show_errors=False): exit_status = 0 for suite in suites: for it in range(iterations): logging.info('Iteration %d of Rendercheck %s suite...' % (it + 1, suite)) (status, passed, total) = \ self._print_test_info(suites=suite, iteration=it + 1, show_errors=show_errors) if status != 0: # Make sure to catch a non-zero exit status logging.info('Iteration %d of Rendercheck %s suite ' 'exited with status %d.' % (it + 1, suite, status)) exit_status = status it += 1 # exit with 1 if passed < total if passed < total: if exit_status == 0: exit_status = 1 return exit_status def get_suites_list(self): '''Return a list of the available test suites''' try: process = Popen(['rendercheck', '--help'], stdout=PIPE, stderr=PIPE, universal_newlines=True) except OSError as exc: if exc.errno == errno.ENOENT: logging.error('Error: please make sure that rendercheck ' 'is installed.') exit(1) else: raise proc = process.communicate()[1].split('\n') found = False tests_pattern = re.compile('.*Available tests: *(.+).*') temp_line = '' tests = [] for line in proc: if found: temp_line += line match = tests_pattern.match(line) if match: first_line = match.group(1).strip().lower() found = True temp_line += first_line for elem in temp_line.split(','): test = elem.strip() if elem: tests.append(test) return tests def main(): # Make sure that we have root privileges if os.geteuid() != 0: print('Error: please run this program as root', file=sys.stderr) exit(1) usage = 'Usage: %prog [OPTIONS]' parser = ArgumentParser(usage) parser.add_argument('-i', '--iterations', type=int, default=10, help='The number of times to run the test. \ Default is 10') parser.add_argument('-d', '--debug', action='store_true', help='Choose this to add verbose output \ for debug purposes') parser.add_argument('-b', '--blacklist', nargs='+', help='Name(s) of rendercheck test(s) to blacklist.') parser.add_argument('-o', '--output', default='', help='The path to the log which will be dumped. \ Default is stdout') parser.add_argument('-tp', '--temp', default='', help='The path where to store temporary files. \ Default is /tmp') args = parser.parse_args() # Set up logging to console format = '%(message)s' console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(format)) # Set up the overall logger logger = logging.getLogger() # This is necessary to ensure debug messages are passed through the logger # to the handler logger.setLevel(logging.DEBUG) # This is what happens when -d and/or -o are passed: # -o -> stdout (info) - log (info) # -d -> only stdout (info and debug) - no log # -d -o -> stdout (info) - log (info and debug) # Write to a log if args.output: # Write INFO to stdout console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) # Specify a log file logfile = args.output logfile_handler = logging.FileHandler(logfile) if args.debug: # Write INFO and DEBUG to a log logfile_handler.setLevel(logging.DEBUG) else: # Write INFO to a log logfile_handler.setLevel(logging.INFO) logfile_handler.setFormatter(logging.Formatter(format)) logger.addHandler(logfile_handler) log_path = os.path.abspath(logfile) # Write only to stdout else: if args.debug: # Write INFO and DEBUG to stdout console_handler.setLevel(logging.DEBUG) logger.addHandler(console_handler) else: # Write INFO to stdout console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) status = 0 rendercheck = RenderCheckWrapper(args.temp) tests = rendercheck.get_suites_list() for test in args.blacklist: if test in tests: tests.remove(test) # Switch between the tty where X lives and tty10 vt_wrap = VtWrapper() target_vt = 10 if vt_wrap.x_vt != target_vt: logging.info('== Vt switch test ==') for it in range(args.iterations): logging.info('Iteration %d...', it) retcode = vt_wrap.set_vt(target_vt) if retcode != 0: logging.error('Error: switching to tty%d failed with code %d ' 'on iteration %d' % (target_vt, retcode, it)) status = 1 else: logging.info('Switching to tty%d: passed' % (target_vt)) time.sleep(2) retcode = vt_wrap.set_vt(vt_wrap.x_vt) if retcode != 0: logging.error('Error: switching to tty%d failed with code %d ' 'on iteration %d' % (vt_wrap.x_vt, retcode, it)) else: logging.info('Switching to tty%d: passed' % (vt_wrap.x_vt)) status = 1 else: logging.error('Error: please run X on a tty other than 10') # Call sleep x times logging.info('== Sleep test ==') sleep_test = SuspendWrapper() sleep_mode = 'mem' # See if we can sleep if sleep_test.can_we_sleep(sleep_mode): for it in range(args.iterations): # Run the test logging.info('Iteration %d...', it + 1) # Set wake time sleep_test.set_wake_time(20) # Suspend to RAM if sleep_test.do_suspend(sleep_mode) == 0: logging.info('Passed') else: logging.error('Failed') status = 1 else: # Skip the test logging.info('Skipped (the system does not seem to support S3') # Rotate the screen x times # The app already rotates the screen 5 times logging.info('== Rotation test ==') rotation_test = RotationWrapper() for it in range(args.iterations): logging.info('Iteration %d...', it + 1) if rotation_test.do_rotation_cycle() == 0: logging.info('Passed') else: logging.error('Failed') status = 1 # Call rendercheck x times logging.info('== Rendercheck test ==') if rendercheck.run_test(tests, args.iterations, args.debug) == 0: logging.info('Passed') else: logging.error('Failed') status = 1 return status if __name__ == '__main__': exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/graphic_memory_info0000775000175000017500000000450712320541306025544 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # graphic_memory_info # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Shawn Wang # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . """ The graphic_memory_info got information from lspci """ import re import sys import subprocess # 00:01.0 VGA compatible controller: \ # Advanced Micro Devices [AMD] nee ATI Wrestler [Radeon HD 6320] \ # (prog-if 00 [VGA controller]) def vgamem_paser(data=None): '''Parsing type vga and find memory information''' device = None vgamems = list() for line in data.split('\n'): for match in re.finditer('(\d\d:\d\d\.\d) VGA(.+): (.+)', line): device = match.group(1) name = match.group(3) if device is None: continue #Memory at e0000000 (32-bit, prefetchable) [size=256M] for match in re.finditer('Memory(.+) prefetchable\) \[size=(\d+)M\]', line): vgamem_size = match.group(2) vgamem = {'device': device, 'name': name, 'vgamem_size': vgamem_size} vgamems.append(vgamem) return vgamems def main(): '''main function lspci -v -s 00:01.0 | grep ' prefetchable' ''' try: data = subprocess.check_output(['lspci', '-v'], universal_newlines=True) except subprocess.CalledProcessError as exc: return exc.returncode vgamems = vgamem_paser(data) for vgamem in vgamems: output_str = "Device({0})\t Name: {1}\tVGA Memory Size: {2}M" print(output_str.format(vgamem['device'], vgamem['name'], vgamem['vgamem_size'])) if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/volume_test0000775000175000017500000001213312320541306024064 0ustar zygazyga00000000000000#!/usr/bin/python3 import sys import re from argparse import ArgumentParser from subprocess import check_output TYPES = ("source", "sink") active_entries_regex = re.compile("\* index.*?(?=properties)", re.DOTALL) entries_regex = re.compile("index.*?(?=properties)", re.DOTALL) index_regex = re.compile("(?<=index: )[0-9]*") muted_regex = re.compile("(?<=muted: ).*") volume_regex = re.compile("(?<=volume: 0: )\s*[0-9]*") name_regex = re.compile("(?<=name:).*") def check_muted(): """ Checks that the active source/sink are not muted. This does not test inactive sources/sinks. """ retval = 0 for vtype in TYPES: try: pacmd_entries = check_output(["pacmd", "list-%ss" % vtype], universal_newlines=True) except Exception as e: print("Error when running pacmd list-%ss: %s" % (vtype, e), file=sys.stderr) return 1 active_entry_match = active_entries_regex.search(pacmd_entries) if active_entry_match: active_entry = active_entry_match.group() else: print("Unable to find a %s active_entry in the pacmd list-%ss"\ " output\npacmd output was: %s" % (vtype, vtype, pacmd_entries), file=sys.stderr) return 1 name_match = name_regex.search(active_entry) if name_match: name = name_match.group() else: print("Unable to determine device bus information from the"\ " pacmd list-%ss output\npacmd output was: %s" % (vtype, pacmd_entries), file=sys.stderr) return 1 muted_match = muted_regex.search(active_entry) if muted_match: muted = muted_match.group().strip() if muted.lower() == "yes": print("FAIL: Audio is muted on %s %s" % (name, vtype)) retval = 1 else: print("PASS: Audio is not muted on %s %s" % (name, vtype)) else: print("Unable to find mute information in the pacmd list-%ss"\ " output for device %s\npacmd output was: %s" % (vtype, name, pacmd_entries), file=sys.stderr) return 1 return retval def check_volume(minvol, maxvol): """ Checks that the volume for all sources/sinks is between min and max. Volume must be < min and > max to pass. """ retval = 0 for vtype in TYPES: try: pacmd_entries = check_output(["pacmd", "list-%ss" % vtype], universal_newlines=True) except Exception as e: print("Error when running pacmd list-%ss: %s" % (vtype, e), file=sys.stderr) return 1 entries = entries_regex.findall(pacmd_entries) for entry in entries: name_match = name_regex.search(entry) if name_match: name = name_match.group() else: print("Unable to determine device bus information from the"\ " pacmd list-%ss output\npacmd output was: %s" % (vtype, pacmd_entries), file=sys.stderr) return 1 volume_match = volume_regex.search(entry) if volume_match: volume = int(volume_match.group().strip()) if volume > maxvol: print ("FAIL: Volume of %d is greater than"\ " maximum of %d for %s %s" % (volume, maxvol, name, vtype)) retval = 1 elif volume < minvol: print ("FAIL: Volume of %d is less than"\ " minimum of %d for %s %s" % (volume, minvol, name, vtype)) retval = 1 else: print ("PASS: Volume is %d for %s %s" % (volume, name, vtype)) else: print("Unable to find volume information in the pacmd"\ " list-%ss output for device %s.\npacmd output "\ "was: %s" % (vtype, name, pacmd_entries), file=sys.stderr) return 1 return retval def main(): parser = ArgumentParser("Check the audio volume") parser.add_argument("-n", "--minvol", type=int, required=True, help="""The minimum volume for a check_volume call. Volume must be greater than this number to be considered a pass.""") parser.add_argument("-x", "--maxvol", type=int, required=True, help="""The maximum volume for a check_volume call. Volume must be less than this number to be considered a pass.""") args = parser.parse_args() check_muted_retval = check_muted() check_volume_retval = check_volume(args.minvol, args.maxvol) return check_muted_retval or check_volume_retval if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/lsmod_info0000775000175000017500000000231712320541306023652 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys from checkbox_support.parsers.modinfo import ModinfoParser from subprocess import Popen, PIPE, check_output, CalledProcessError def main(): process = Popen('lsmod', stdout=PIPE, stderr=PIPE, universal_newlines=True) data = process.stdout.readlines() # Delete the first item because it's headers from lsmod output data.pop(0) module_list = [module.split()[0].strip() for module in data] cmd = '/sbin/modinfo' for module in sorted(module_list): version = '' stream = b'' try: stream = check_output([cmd, module], stderr=PIPE, universal_newlines=False) except CalledProcessError as e: if e.returncode != 1: raise e else: version = 'Unavailable' stream = stream.decode('utf-8') parser = ModinfoParser(stream) if not version: version = parser.get_field('version') if not version: version = parser.get_field('vermagic').split()[0] print('%s: %s' % (module, version)) return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/virtualization0000775000175000017500000002640212320541306024606 0ustar zygazyga00000000000000#!/usr/bin/env python3 """ Script to test virtualization functionality Copyright (C) 2013, 2014 Canonical Ltd. Authors Jeff Marcom Daniel Manrique This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ from argparse import ArgumentParser import configparser import os import re import logging import lsb_release import shlex import signal from subprocess import ( Popen, PIPE, CalledProcessError, check_output, call ) import sys import tempfile import tarfile import time import urllib.request DEFAULT_TIMEOUT = 500 class XENTest(object): pass class KVMTest(object): def __init__(self, image=None, timeout=500, debug_file="virt_debug"): self.image = image self.timeout = timeout self.debug_file = os.path.join(os.getcwd(), debug_file) self.arch = check_output(['arch'], universal_newlines=True) def download_image(self): """ Downloads Cloud image for same release as host machine """ # Check Ubuntu release info. Example {quantal, precise} release = lsb_release.get_lsb_information()["CODENAME"] # Construct URL cloud_url = "http://cloud-images.ubuntu.com" if re.match("arm.*", self.arch): cloud_iso = release + "-server-cloudimg-armhf.tar.gz" else: cloud_iso = release + "-server-cloudimg-i386-disk1.img" image_url = "/".join(( cloud_url, release, "current", cloud_iso)) logging.debug("Downloading {}, from {}".format(cloud_iso, cloud_url)) # Attempt download try: resp = urllib.request.urlretrieve(image_url, cloud_iso) except (IOError, OSError, urllib.error.HTTPError) as exception: logging.error("Failed download of image from %s: %s", image_url, exception) return False # Unpack img file from tar if re.match("arm.*", self.arch): cloud_iso_tgz = tarfile.open(cloud_iso) cloud_iso = cloud_iso.replace('tar.gz', 'img') cloud_iso_tgz.extract(cloud_iso) if not os.path.isfile(cloud_iso): return False return cloud_iso def boot_image(self, data_disk): """ Attempts to boot the newly created qcow image using the config data defined in config.iso """ logging.debug("Attempting boot for:{}".format(data_disk)) # Set Arbitrary IP values netrange = "10.0.0.0/8" image_ip = "10.0.0.1" hostfwd = "tcp::2222-:22" cloud_disk = "" # Should we attach the cloud config disk if os.path.isfile("seed.iso"): logging.debug("Attaching Cloud config disk") cloud_disk = "-drive file=seed.iso,if=virtio" if re.match("(arm.*|aarch64)", self.arch): uname = check_output(['uname', '-r'], universal_newlines=True) cloud_disk = cloud_disk.replace("virtio", "sd") params = 'qemu-system-arm -machine vexpress-a15 -cpu cortex-a15 -enable-kvm -m {} -kernel /boot/vmlinuz -append "console=ttyAMA0 earlyprintk=serial root=/dev/mmcblk0 ro rootfstype=ext4" -serial stdio -dtb /lib/firmware/{}/device-tree/vexpress-v2p-ca15-tc1.dtb -initrd /boot/initrd.img -net nic -net user,net={},host={},hostfwd={} -drive file={},if=sd,cache=writeback {} -display none -nographic'.format(uname, "256", netrange, image_ip, hostfwd, data_disk, cloud_disk) else: params = \ ''' qemu-system-x86_64 -machine accel=kvm:tcg -m {0} -net nic -net user,net={1},host={2},hostfwd={3} -drive file={4},if=virtio {5} -display none -nographic '''.format( "256", netrange, image_ip, hostfwd, data_disk, cloud_disk).replace("\n", "").replace(" ", "") logging.debug("Using params:{}".format(params)) # Default file location for log file is in checkbox output directory checkbox_dir = os.getenv("CHECKBOX_DATA") if checkbox_dir is not None: self.debug_file = os.path.join(checkbox_dir, self.debug_file) logging.info("Storing VM console output in {}".format( os.path.realpath(self.debug_file))) # Open VM STDERR/STDOUT log file for writing try: file = open(self.debug_file, 'w') except IOError: logging.error("Failed creating file:{}".format(self.debug_file)) return False # Start Virtual machine self.process = Popen( shlex.split(params), stdin=PIPE, stderr=file, stdout=file, universal_newlines=True, shell=False) def create_cloud_disk(self): """ Generate Cloud meta data and creates an iso object to be mounted as virtual device to instance during boot. """ user_data = """\ #cloud-config runcmd: - [ sh, -c, echo "========= CERTIFICATION TEST =========" ] power_state: mode: halt message: Bye timeout: 480 final_message: CERTIFICATION BOOT COMPLETE """ meta_data = """\ { echo instance-id: iid-local01; echo local-hostname, certification; } """ for file in ['user-data', 'meta-data']: logging.debug("Creating cloud %s", file) with open(file, "wt") as data_file: os.fchmod(data_file.fileno(), 0o777) data_file.write(vars()[file.replace("-", "_")]) # Create Data ISO hosting user & meta cloud config data try: iso_build = check_output( ['genisoimage', '-output', 'seed.iso', '-volid', 'cidata', '-joliet', '-rock', 'user-data', 'meta-data'], universal_newlines=True) except CalledProcessError as exception: logging.exception("Cloud data disk creation failed") def start(self): logging.debug('Starting KVM Test') status = 1 # Create temp directory: date = time.strftime("%b_%d_%Y_") with tempfile.TemporaryDirectory("_kvm_test", date) as temp_dir: os.chmod(temp_dir, 0o744) os.chdir(temp_dir) if not self.image: logging.debug('No image specified, downloading one now.') # Download cloud image self.image = self.download_image() if self.image and os.path.isfile(self.image): if "cloud" in self.image: # Will assume we need to supply cloud meta data # for instance boot to be successful self.create_cloud_disk() # Boot Virtual Machine instance = self.boot_image(self.image) time.sleep(self.timeout) # Reset Console window to regain control from VM Serial I/0 call('reset') # Check to be sure VM boot was successful with open(self.debug_file, 'r') as debug_file: file_contents = debug_file.read() if "CERTIFICATION BOOT COMPLETE" in file_contents: if "END SSH HOST KEY KEYS" in file_contents: print("Booted successfully", file=sys.stderr) else: print("Booted successfully (Previously " "initalized VM)", file=sys.stderr) status = 0 else: print("E: KVM instance failed to boot", file=sys.stderr) print("Console output".center(72, "="), file=sys.stderr) with open(self.debug_file, 'r') as console_log: print(console_log.read(), file=sys.stderr) print("E: KVM instance failed to boot", file=sys.stderr) self.process.terminate() elif not self.image: print("Could not find downloaded image") else: print("Could not find: {}".format(self.image), file=sys.stderr) return status def test_kvm(args): print("Executing KVM Test", file=sys.stderr) DEFAULT_CFG = "/etc/checkbox.d/virtualization.cfg" image = "" timeout = "" # Configuration data can come from three sources. # Lowest priority is the config file. config_file = DEFAULT_CFG config = configparser.SafeConfigParser() try: config.readfp(open(config_file)) except IOError: logging.warn("No config file found") else: try: timeout = config.getfloat("KVM", "timeout") except ValueError: logging.warning('Invalid or Empty timeout in config file. ' 'Falling back to default') except configparser.NoSectionError as e: logging.exception(e) try: image = config.get("KVM", "image") except configparser.NoSectionError: logging.exception('Invalid or Empty image in config file.') # Next in priority are environment variables. if 'KVM_TIMEOUT' in os.environ: try: timeout = float(os.environ['KVM_TIMEOUT']) except ValueError as err: logging.warning("TIMEOUT env variable: %s" % err) timeout = DEFAULT_TIMEOUT if 'KVM_IMAGE' in os.environ: image = os.environ['KVM_IMAGE'] # Finally, highest-priority are command line arguments. if args.timeout: timeout = args.timeout elif not timeout: timeout = DEFAULT_TIMEOUT if args.image: image = args.image kvm_test = KVMTest(image, timeout) result = kvm_test.start() sys.exit(result) def main(): parser = ArgumentParser(description="Virtualization Test") subparsers = parser.add_subparsers() # Main cli options kvm_test_parser = subparsers.add_parser( 'kvm', help=("Run kvm virtualization test")) #xen_test_parser = subparsers.add_parser('xen', # help=("Run xen virtualization test")) # Sub test options kvm_test_parser.add_argument( '-i', '--image', type=str, default=None) kvm_test_parser.add_argument( '-t', '--timeout', type=int) kvm_test_parser.add_argument('--debug', dest='log_level', action="store_const", const=logging.DEBUG, default=logging.INFO) kvm_test_parser.set_defaults(func=test_kvm) args = parser.parse_args() try: logging.basicConfig(level=args.log_level) except AttributeError: pass # avoids exception when trying to run without specifying 'kvm' # to check if not len(sys.argv) > 1 if len(vars(args)) == 0: parser.print_help() return False args.func(args) if __name__ == "__main__": main() 2013.com.canonical.certification.checkbox-0.4/bin/sleep_time_check0000775000175000017500000000414412320541306025004 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument('filename', action='store', help='The output file from sleep tests to parse') parser.add_argument('--s', dest='sleep_threshold', action='store', type=float, default=10.00, help=('The max time a system should have taken to ' 'enter a sleep state. (Default: %(default)s)' )) parser.add_argument('--r', action='store', dest='resume_threshold', type=float, default=3.00, help=('The max time a system should have taken to ' 'resume from a sleep state. (Default: ' '%(default)s)')) args = parser.parse_args() try: file = open(args.filename) lines = file.readlines() finally: file.close() # find our times for line in lines: if "Average time to sleep" in line: sleep_time = float(line.split(':')[1].strip()) elif "Average time to resume" in line: resume_time = float(line.split(':')[1].strip()) print("Average time to enter sleep state: %s seconds" % sleep_time) print("Average time to resume from sleep state: %s seconds" % resume_time) failed = False if sleep_time > args.sleep_threshold: print("System failed to suspend in less than %s seconds" % args.sleep_threshold) failed = True if resume_time > args.resume_threshold: print("System failed to resume in less than %s seconds" % args.resume_threshold) failed = True if sleep_time <= 0.00 or resume_time <= 0.00: print("ERROR: One or more times was not reported correctly") failed = True return failed if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/pulse-active-port-change0000775000175000017500000001151412320541306026326 0ustar zygazyga00000000000000#!/usr/bin/env python3 # This file is part of Checkbox. # # Copyright 2014 Canonical Ltd. # Written by: # Zygmunt Krynicki # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . """ pulse-active-port-change ======================== This script checks if the active port on either sinks (speakers or headphones) or sources (microphones, webcams) is changed after an appropriate device is plugged into the DUT. The script is fully automatic and either times out after 30 seconds or returns as soon as the change is detected. The script monitors pulse audio events with `pactl subscribe`. Any changes to sinks (or sources, depending on the mode) are treated as a possible match. A match is verified by running `pactl list sinks` (or `pactl list sources`) and constructing a set of pairs (sink-source-name, sink-source-active-port). Any change to the computed set, as compared to the initially computed set, is considered a match. Due to the algorithm used, it will also detect things like USB headsets, HDMI monitors/speakers, webcams, etc. The script depends on: python3-checkbox-support Which depends on: python3-pyparsing """ import argparse import os import pty import signal import subprocess from checkbox_support.parsers.pactl import parse_pactl_output class AudioPlugDetection: def __init__(self, timeout, mode): # store parameters self.timeout = timeout self.mode = mode # get the un-localized environment env = dict(os.environb) env[b'LANG'] = b'' env[b'LANGUAGE'] = b'' env[b'LC_ALL'] = b'C.UTF-8' self.unlocalized_env = env # set SIGALRM handler signal.signal(signal.SIGALRM, self.on_timeout) def get_sound_config(self): text = subprocess.check_output( ["pactl", "list", self.mode], # either 'sources' or 'sinks' env=self.unlocalized_env, universal_newlines=True) doc = parse_pactl_output(text) cfg = set() for record in doc.record_list: for attr in record.attribute_list: if attr.name == "Active Port": cfg.add((record.name, attr.value)) return cfg def on_timeout(self, signum, frame): print("Time is up") raise SystemExit(1) @classmethod def main(cls): parser = argparse.ArgumentParser( description=__doc__.split(" ")[0], epilog=__doc__.split(" ")[1], formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( 'mode', choices=['sinks', 'sources'], help='Monitor either sinks or sources') parser.add_argument( '-t', '--timeout', type=int, default=30, help='Timeout after which the script fails') ns = parser.parse_args() return cls(ns.timeout, ns.mode).run() def run(self): found = False if self.mode == 'sinks': look_for = "Event 'change' on sink #" elif self.mode == 'sources': look_for = "Event 'change' on source #" else: assert False # Get the initial / baseline configuration initial_cfg = self.get_sound_config() print("Starting with config: {}".format(initial_cfg)) print("You have {} seconds to plug something".format(self.timeout)) # Start the timer signal.alarm(self.timeout) # run subscribe in a pty as it doesn't fflush() after every event pid, master_fd = pty.fork() if pid == 0: os.execlpe("pactl", "pactl", "subscribe", self.unlocalized_env) else: child_stream = os.fdopen(master_fd, "rt", encoding='UTF-8') try: for line in child_stream: if line.startswith(look_for): new_cfg = self.get_sound_config() print("Now using config: {}".format(new_cfg)) if new_cfg != initial_cfg: print("It seems to work!") found = True break except KeyboardInterrupt: pass finally: os.kill(pid, signal.SIGTERM) os.close(master_fd) return 0 if found else 1 if __name__ == "__main__": raise SystemExit(AudioPlugDetection.main()) 2013.com.canonical.certification.checkbox-0.4/bin/dns_server_test0000775000175000017500000000256312320541306024735 0ustar zygazyga00000000000000#!/bin/bash # # Verify dns server setup # Packages and ports can be verified offline but external connections # require a network connection. The script will check for this. # # Verify process is running. Expected output 'pgrep named' is a $pid check=`pgrep named` if [ -z "$check" ]; then echo "FAIL: DNS bind is not running." exit 1 fi # Check ports result1=`host www.ubuntu.com localhost | grep "#53"` result2=`host -T -6 www.ubuntu.com ip6-localhost | grep "#53"` if [ -z "$result1" ]; then echo "FAIL: DNS is not using port 53." exit 1 elif [ -z "$result2" ]; then echo "FAIL: DNS is not using port 53 on IPv6." exit 1 fi # Check if udp is established udpCheck=`netstat -auvpn | egrep '(:53)' |egrep 'udp'` if [ -z "$udpCheck" ]; then echo "FAIL: DNS udp setup is not established." exit 1 fi # Check if external dns queries work but first verify the network # is up and running check=`ping -c 2 www.ubuntu.com |grep "2 received"` if [ -n "$check" ]; then failure="2(SERVFAIL)" result1=`host www.ubuntu.com localhost | grep "SERVFAIL" |awk '{print $5}'` result2=`host -T -6 www.ubuntu.com ip6-localhost | grep "SERVFAIL" |awk '{print $5}'` if [ "$result1" = $failure ]; then echo "FAIL: DNS external connection fails." exit 1 elif [ "$result2" = $failure ]; then echo "FAIL: DNS external connection via IPv6 fails." exit 1 fi fi exit 0 2013.com.canonical.certification.checkbox-0.4/bin/cpu_topology0000775000175000017500000000645312320541306024251 0ustar zygazyga00000000000000#!/usr/bin/env python3 ''' cpu_topology Written by Jeffrey Lane ''' import sys import os class proc_cpuinfo(): ''' Class to get and handle information from /proc/cpuinfo Creates a dictionary of data gleaned from that file. ''' def __init__(self): self.cpuinfo = {} cpu_fh = open('/proc/cpuinfo', 'r') try: temp = cpu_fh.readlines() finally: cpu_fh.close() for i in temp: if i.startswith('processor'): key = 'cpu' + (i.split(':')[1].strip()) self.cpuinfo[key] = {'core_id':'', 'physical_package_id':''} elif i.startswith('core id'): self.cpuinfo[key].update({'core_id': i.split(':')[1].strip()}) elif i.startswith('physical id'): self.cpuinfo[key].update({'physical_package_id': i.split(':')[1].strip()}) else: continue class sysfs_cpu(): ''' Class to get and handle information from sysfs as relates to CPU topology Creates an informational class to present information on various CPUs ''' def __init__(self, proc): self.syscpu = {} self.path = '/sys/devices/system/cpu/' + proc + '/topology' items = ['core_id', 'physical_package_id'] for i in items: syscpu_fh = open(os.path.join(self.path, i), 'r') try: self.syscpu[i] = syscpu_fh.readline().strip() finally: syscpu_fh.close() def compare(proc_cpu, sys_cpu): cpu_map = {} ''' If there is only 1 CPU the test don't look for core_id and physical_package_id because those information are absent in /proc/cpuinfo on singlecore system ''' for key in proc_cpu.keys(): if 'cpu1' not in proc_cpu: cpu_map[key] = True else: for subkey in proc_cpu[key].keys(): if proc_cpu[key][subkey] == sys_cpu[key][subkey]: cpu_map[key] = True else: cpu_map[key] = False return cpu_map def main(): cpuinfo = proc_cpuinfo() sys_cpu = {} keys = cpuinfo.cpuinfo.keys() for k in keys: sys_cpu[k] = sysfs_cpu(k).syscpu cpu_map = compare(cpuinfo.cpuinfo, sys_cpu) if False in cpu_map.values() or len(cpu_map) < 1: print("FAIL: CPU Topology is incorrect", file=sys.stderr) print("-" * 52, file=sys.stderr) print("{0}{1}".format("/proc/cpuinfo".center(30), "sysfs".center(25)), file=sys.stderr) print("{0}{1}{2}{3}{1}{2}".format( "CPU".center(6), "Physical ID".center(13), "Core ID".center(9), "|".center(3)), file=sys.stderr) for key in sorted(sys_cpu.keys()): print("{0}{1}{2}{3}{4}{5}".format( key.center(6), cpuinfo.cpuinfo[key]['physical_package_id'].center(13), cpuinfo.cpuinfo[key]['core_id'].center(9), "|".center(3), sys_cpu[key]['physical_package_id'].center(13), sys_cpu[key]['core_id'].center(9)), file=sys.stderr) return 1 else: return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/piglit_test0000775000175000017500000000614112320541306024047 0ustar zygazyga00000000000000#!/usr/bin/python3 import os import sys from argparse import ArgumentParser, FileType from subprocess import check_output, STDOUT class PiglitTests: def __init__(self, tests, name): self._tests = tests self._name = name self._results = {} def run(self): piglit_output = '' log_path = os.path.join(os.environ.get('CHECKBOX_DATA', '.'), 'piglit-results', self._name) run_command = ["piglit-run.py"] for test in self._tests: run_command.extend(["-t", test]) run_command.extend(['/usr/share/piglit/tests/all.tests', log_path]) piglit_output = check_output(run_command, universal_newlines=True, stderr=STDOUT) # TODO: Capture stderr instead? for line in piglit_output.split('\n'): if ' :: ' in line: self._results[line.split(' :: ')[-1].strip()] = \ line.split(' :: ')[-2].strip() def get_tests_by_status(self, status): """ Return a list of the tests with the given status in the last piglit run """ tests = [] for test in self._results: if self._results[test] == status: tests.append(test) return tests def main(): parser = ArgumentParser("A wrapper script for the Piglit graphics test " "framework which runs the tests and parses the " "results.") parser.add_argument("--test", "-t", required=True, action='append', help="The expression used to get the tests to run.") parser.add_argument("--name", "-n", required=True, help="""A friendly name for this group of tests to use in reporting.""") parser.add_argument("--verbose", "-v", action='store_true', help='Have more verbose output') args = parser.parse_args() piglit = PiglitTests(args.test, args.name) piglit.run() passed_tests = piglit.get_tests_by_status('pass') print("%d tests passed" % len(passed_tests)) if args.verbose: print("\n".join(["- %s" % test for test in passed_tests])) failed_tests = piglit.get_tests_by_status('fail') if failed_tests: print("%d tests failed" % len(failed_tests)) print("\n".join(["- %s" % test for test in failed_tests])) crashed_tests = piglit.get_tests_by_status('crash') if crashed_tests: print("%d tests crashed" % len(crashed_tests)) print("\n".join(["- %s" % test for test in crashed_tests])) skipped_tests = piglit.get_tests_by_status('skip') if skipped_tests: print("%d tests were skipped" % len(skipped_tests)) print("\n".join(["- %s" % test for test in skipped_tests])) if len(failed_tests) > 0 or len(crashed_tests) > 0: return 1 else: return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network_info0000775000175000017500000000255712320541306024233 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import socket import fcntl import struct SYS_PATH = '/sys/class/net' def _read_file(file): source = open(file, 'r') content = source.read() source.close() return content def get_connected(interface): STATUS = ('No', 'Yes') carrier_file = os.path.join(SYS_PATH, interface, 'carrier') carrier = 0 try: carrier = int(_read_file(carrier_file)) except IOError: pass return STATUS[carrier] def get_ip_address(interface): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) return socket.inet_ntoa(fcntl.ioctl( s.fileno(), 0x8915, # SIOCGIFADDR struct.pack('256s', interface[:15].encode()) )[20:24]) def get_mac_address(interface): address_file = os.path.join(SYS_PATH, interface, 'address') address = '' try: address = _read_file(address_file) except IOError: pass return address def main(args): for interface in args: connected = get_connected(interface) print("Interface: %s" % interface) print("Connected: %s" % connected) try: print("IP: %s" % get_ip_address(interface)) except IOError: print("IP: n/a") print("MAC: %s\n" % get_mac_address(interface)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/bluetooth_scan0000775000175000017500000000026112320541306024526 0ustar zygazyga00000000000000#!/bin/bash hciconfig hci0 reset name=`zenity --title="Bluetooth Send" --entry --text="Bluetooth Send"` address=`hcitool scan | grep "$name" | awk '{print $1}'` echo $address 2013.com.canonical.certification.checkbox-0.4/bin/udisks2_monitor0000775000175000017500000001432112320541306024652 0ustar zygazyga00000000000000#!/usr/bin/env python3 # Copyright 2012 Canonical Ltd. # Written by: # Zygmunt Krynicki # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Script that observes changes to various devices, as published by UDisks2. Note: this script has no UDisks (one) equivalent. """ import errno import logging import sys from checkbox_support.dbus import connect_to_system_bus, drop_dbus_type from checkbox_support.dbus.udisks2 import UDisks2Observer from dbus.exceptions import DBusException def _print_interfaces_and_properties(interfaces_and_properties): """ Print a collection of interfaces and properties exported by some object The argument is the value of the dictionary _values_, as returned from GetManagedObjects() for example. See this for details: http://dbus.freedesktop.org/doc/dbus-specification.html# standard-interfaces-objectmanager """ for interface_name, properties in interfaces_and_properties.items(): print(" - Interface {}".format(interface_name)) for prop_name, prop_value in properties.items(): prop_value = drop_dbus_type(prop_value) print(" * Property {}: {}".format(prop_name, prop_value)) def main(): # Connect to the system bus, we also get the event # loop as we need it to start listening for signals. system_bus, loop = connect_to_system_bus() # Create an instance of the observer that we'll need for the model observer = UDisks2Observer() # Define all our callbacks in advance, there are three callbacks that we # need, for interface insertion/removal (which roughly corresponds to # objects/devices coming and going) and one extra signal that is only fired # once, when we get the initial list of objects. # Let's print everything we know about initially for the users to see def print_initial_objects(managed_objects): print("UDisks2 knows about the following objects:") for object_path, interfaces_and_properties in managed_objects.items(): print(" * {}".format(object_path)) _print_interfaces_and_properties(interfaces_and_properties) sys.stdout.flush() observer.on_initial_objects.connect(print_initial_objects) # Setup a callback for the InterfacesAdded signal. This way we will get # notified of any interface changes in this collection. In practice this # means that all objects that are added/removed will be advertised through # this mechanism def print_interfaces_added(object_path, interfaces_and_properties): print("The object:") print(" {}".format(object_path)) print("has gained the following interfaces and properties:") _print_interfaces_and_properties(interfaces_and_properties) sys.stdout.flush() observer.on_interfaces_added.connect(print_interfaces_added) # Setup a callback on PropertiesChanged signal. This way we will get # notified on any changes to the values of properties exported by various # objects on the bus. def print_properties_changed(object_path, interface_name, changed_properties, invalidated_properties): print("The object") print(" {}".format(object_path)) print("has changed the following properties") print(" - Interface {}".format(interface_name)) for prop_name, prop_value in changed_properties.items(): prop_value = drop_dbus_type(prop_value) print(" * Property {}: {}".format(prop_name, prop_value)) for prop_name in invalidated_properties: print(" * Property {} (invalidated)".format(prop_name)) observer.on_properties_changed.connect(print_properties_changed) # Again, a similar callback for interfaces that go away. It's not spelled # out explicitly but it seems that objects with no interfaces left are # simply gone. We'll treat them as such def print_interfaces_removed(object_path, interfaces): print("The object:") print(" {}".format(object_path)) print("has lost the following interfaces:") for interface in interfaces: print(" * {}".format(interface)) sys.stdout.flush() observer.on_interfaces_removed.connect(print_interfaces_removed) # Now that all signal handlers are set, connect the observer to the system # bus try: logging.debug("Connecting UDisks2 observer to DBus") observer.connect_to_bus(system_bus) except DBusException as exc: # Manage the missing service error if needed to give sensible error # message on precise where UDisks2 is not available if exc.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown": print("You need to have udisks2 installed to run this program") print("It is only applicable to Ubuntu 12.10, or later") raise SystemExit(1) else: raise # main_shield() will catch this one # Now start the event loop and just display any device changes print("=" * 80) print("Waiting for device changes (press ctlr+c to exit)") print("=" * 80) logging.debug("Entering event loop") sys.stdout.flush() # Explicitly flush to allow tee users to see things try: loop.run() except KeyboardInterrupt: loop.quit() print("Exiting") def main_shield(): """ Helper for real main that manages exceptions we don't recover from """ try: main() except DBusException as exc: logging.exception("Caught fatal DBus exception, aborting") except IOError as exc: # Ignore pipe errors as they are harmless if exc.errno != errno.EPIPE: raise if __name__ == "__main__": main_shield() 2013.com.canonical.certification.checkbox-0.4/bin/samba_test0000775000175000017500000000122712320541306023642 0ustar zygazyga00000000000000#!/bin/bash # # Confirm Samba service is running # Requires: samba winbind # #Verify Samba processes are running smbd=`pgrep smbd` if [ -z "$smbd" ]; then echo "FAIL: smbd is not running." exit 1 fi nmbd=`pgrep nmbd` if [ -z "$nmbd" ]; then echo "FAIL: nmbd is not running." exit 1 fi winbindd=`pgrep winbindd` if [ -z "$winbindd" ]; then echo "FAIL: winbindd is not running." exit 1 fi sid=`net getlocalsid | grep "S-1-5"` #req. root if [ -z "$sid" ]; then echo "FAIL: Default samba workgroup is not set." exit 1 fi users=`net usersidlist | grep "UBUNTU"` if [ -z "$sid" ]; then echo "FAIL: samba userId is not set." exit 1 fi exit 0 2013.com.canonical.certification.checkbox-0.4/bin/cking_suite0000775000175000017500000000547512320541306024035 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import posixpath import subprocess from optparse import OptionParser DEFAULT_DIRECTORY = "/tmp/checkbox.cking-scripts" DEFAULT_LOCATION = "git://kernel.ubuntu.com/cking/scripts" COMMAND_TEMPLATE = "cd %(scripts)s; ./%(script)s" def print_line(key, value): if type(value) is list: print("%s:" % key) for v in value: print(" %s" % v) else: print("%s: %s" % (key, value)) def print_element(element): for key, value in element.items(): print_line(key, value) print() def clone_cking_scripts(location, directory): if posixpath.exists(directory): return dirname = posixpath.dirname(directory) if not posixpath.exists(dirname): os.makedirs(dirname) process = subprocess.Popen(["git", "clone", location, directory], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() if process.wait(): raise Exception("Failed to clone cking scripts from %s" % location) def run_cking_scripts(scripts, location, directory, dry_run): if not dry_run: clone_cking_scripts(location, directory) for script in scripts: path = posixpath.join(directory, script) # Initialize test structure test = {} test["plugin"] = "shell" test["name"] = posixpath.splitext(posixpath.basename(path))[0] test["command"] = COMMAND_TEMPLATE % { "scripts": posixpath.dirname(path), "script": posixpath.basename(path)} # Get description from first line of the README file readme_path = posixpath.join(posixpath.dirname(path), "README") if os.path.exists(readme_path): file = open(readme_path) test["description"] = file.readline().strip() file.close else: test["description"] = "No description found" yield test def main(args): usage = "Usage: %prog [OPTIONS] [SCRIPTS]" parser = OptionParser(usage=usage) parser.add_option("--dry-run", default=False, action="store_true", help="Dry run to avoid branching from the given location.") parser.add_option("-d", "--directory", default=DEFAULT_DIRECTORY, help="Directory where to branch qa-regression-testing") parser.add_option("-l", "--location", default=DEFAULT_LOCATION, help="Location from where to branch qa-regression-testing") (options, scripts) = parser.parse_args(args) if not scripts: parser.error("Must specify a script") tests = run_cking_scripts(scripts, options.location, options.directory, options.dry_run) if not tests: return 1 for test in tests: print_element(test) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/optical_read_test0000775000175000017500000000746412320541306025216 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import posixpath import filecmp import shutil from argparse import ArgumentParser from subprocess import Popen, PIPE DEFAULT_DIR = '/tmp/checkbox.optical' DEFAULT_DEVICE_DIR = 'device' DEFAULT_IMAGE_DIR = 'image' CDROM_ID = '/lib/udev/cdrom_id' def _command(command, shell=True): proc = Popen(command, shell=shell, stdout=PIPE, stderr=PIPE ) return proc def _command_out(command, shell=True): proc = _command(command, shell) return proc.communicate()[0].strip() def compare_tree(source, target): for dirpath, dirnames, filenames in os.walk(source): #if both tree are empty return false if dirpath == source and dirnames == [] and filenames == []: return False for name in filenames: file1 = os.path.join(dirpath, name) file2 = file1.replace(source, target, 1) if os.path.isfile(file1) and not os.path.islink(file1): if filecmp.cmp(file1, file2): continue else: return False else: continue return True def read_test(device): passed = False device_dir = os.path.join(DEFAULT_DIR, DEFAULT_DEVICE_DIR) image_dir = os.path.join(DEFAULT_DIR, DEFAULT_IMAGE_DIR) for dir in (device_dir, image_dir): if posixpath.exists(dir): shutil.rmtree(dir) os.makedirs(device_dir) try: _command("umount %s" % device).communicate() mount = _command("mount -o ro %s %s" % (device, device_dir)) mount.communicate() if mount.returncode != 0: print("Unable to mount %s to %s" % (device, device_dir), file=sys.stderr) return False file_copy = _command("cp -dpR %s %s" % (device_dir, image_dir)) file_copy.communicate() if file_copy.returncode != 0: print("Failed to copy files from %s to %s" % (device_dir, image_dir), file=sys.stderr) return False if compare_tree(device_dir, image_dir): passed = True except: print("File Comparison failed while testing %s" % device, file=sys.stderr) passed = False finally: _command("umount %s" % device_dir).communicate(3) for dir in (device_dir, image_dir): if posixpath.exists(dir): shutil.rmtree(dir) if passed: print("File Comparison passed (%s)" % device) return passed def get_capabilities(device): cmd = "%s %s" % (CDROM_ID, device) capabilities = _command_out(cmd) return capabilities def main(): tests = [] return_values = [] parser = ArgumentParser() parser.add_argument("device", nargs='+', help=('Specify an optical device or list of devices ' 'such as /dev/cdrom')) args = parser.parse_args() if os.geteuid() != 0: parser.error("ERROR: Must be root to run this script.") for device in args.device: capabilities = get_capabilities(device) if not capabilities: print("Unable to get capabilities of %s" % device, file=sys.stderr) return 1 for capability in capabilities.decode().split('\n'): if capability[:3] == 'ID_': cap = capability[3:-2] if cap == 'CDROM' or cap == 'CDROM_DVD': tests.append('read') for test in set(tests): print("Testing %s on %s ... " % (test, device), file=sys.stdout) tester = "%s_test" % test return_values.append(globals()[tester](device)) return False in return_values if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/wireless_ext0000775000175000017500000000040412320541306024231 0ustar zygazyga00000000000000#!/bin/sh if lsmod | grep "80211" &> /dev/null ; then echo "Driver is using MAC80211" if iwconfig 2>&1 | grep "IEEE 802.11" &> /dev/null ; then echo "Driver has loaded wireless extension" exit 0 else echo "No wireless module loaded" exit 1 fi fi 2013.com.canonical.certification.checkbox-0.4/bin/touchpad_driver_info0000775000175000017500000000433712320541306025722 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys from io import StringIO from subprocess import Popen, PIPE, check_output, STDOUT, CalledProcessError from checkbox_support.parsers.udevadm import UdevadmParser from checkbox_support.parsers.modinfo import ModinfoParser # Command to retrieve udev information. COMMAND = 'udevadm info --export-db' class TouchResult: attributes = {} def addDevice(self, device): if getattr(device, 'category') == 'TOUCH': self.attributes['driver'] = getattr(device, 'driver') self.attributes['product'] = getattr(device, 'product') class TouchpadDriver(): def __init__(self, driver): self._driver = driver self.driver_version = self._find_driver_version() def _find_driver_version(self): cmd = ['/sbin/modinfo', self._driver] try: stream = check_output(cmd, stderr=STDOUT, universal_newlines=True) except CalledProcessError as err: print("Error communicating with modinfo.") print(err.output) return None if not stream: print("Error: modinfo returned nothing.") else: parser = ModinfoParser(stream) version = parser.get_field('version') if not version: version = parser.get_field('vermagic').split()[0] return version def get_touch_attributes(): output, err = Popen(COMMAND, stdout=PIPE, shell=True).communicate() if err: print("Error running $s" % ' '.join(COMMAND)) print(err) return None udev = UdevadmParser(StringIO(output.decode("unicode-escape"))) result = TouchResult() udev.run(result) return result.attributes def main(): attributes = get_touch_attributes() if attributes: modinfo = TouchpadDriver(attributes['driver']) attributes['version'] = modinfo.driver_version print("%s: %s\n%s: %s\n%s: %s\n" % ('Device', attributes['product'], 'Driver', attributes['driver'], 'Driver Version', attributes['version'])) else: print("No Touchpad Detected") return 1 return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/rendercheck_test0000775000175000017500000002202412320541306025032 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # rendercheck_test # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . from subprocess import Popen, PIPE from argparse import ArgumentParser import logging import os import re import tempfile import errno class RenderCheck(object): """A simple class to run the rendercheck suites""" def __init__(self, temp_dir=None): self._temp_dir = temp_dir def _print_test_info(self, suites='all', iteration=1, show_errors=False): '''Print the output of the test suite''' main_command = 'rendercheck' passed = 0 total = 0 if self._temp_dir: # Use the specified path temp_file = tempfile.NamedTemporaryFile(dir=self._temp_dir, delete=False) else: # Use /tmp temp_file = tempfile.NamedTemporaryFile(delete=False) if suites == all: full_command = [main_command, '-f', 'a8r8g8b8'] else: full_command = [main_command, '-t', suites, '-f', 'a8r8g8b8'] try: # Let's dump the output into file as it can be very large # and we don't want to store it in memory process = Popen(full_command, stdout=temp_file, universal_newlines=True) except OSError as exc: if exc.errno == errno.ENOENT: logging.error('Error: please make sure that rendercheck ' 'is installed.') exit(1) else: raise exit_code = process.wait() temp_file.close() # Read values from the file errors = re.compile('.*test error.*') results = re.compile('(.+) tests passed of (.+) total.*') first_error = True with open(temp_file.name) as temp_handle: for line in temp_handle: match_output = results.match(line) match_errors = errors.match(line) if match_output: passed = int(match_output.group(1).strip()) total = int(match_output.group(2).strip()) logging.info('Results:') logging.info(' %d tests passed out of %d.' % (passed, total)) if show_errors and match_errors: error = match_errors.group(0).strip() if first_error: logging.debug('Rendercheck %s suite errors ' 'from iteration %d:' % (suites, iteration)) first_error = False logging.debug(' %s' % error) # Remove the file os.unlink(temp_file.name) return (exit_code, passed, total) def run_test(self, suites=[], iterations=1, show_errors=False): exit_status = 0 for suite in suites: for it in range(iterations): logging.info('Iteration %d of Rendercheck %s suite...' % (it + 1, suite)) (status, passed, total) = \ self._print_test_info(suites=suite, iteration=it + 1, show_errors=show_errors) if status != 0: # Make sure to catch a non-zero exit status logging.info('Iteration %d of Rendercheck %s suite ' 'exited with status %d.' % (it + 1, suite, status)) exit_status = status it += 1 # exit with 1 if passed < total if passed < total: if exit_status == 0: exit_status = 1 return exit_status def get_suites_list(self): '''Return a list of the available test suites''' try: process = Popen(['rendercheck', '--help'], stdout=PIPE, stderr=PIPE, universal_newlines=True) except OSError as exc: if exc.errno == errno.ENOENT: logging.error('Error: please make sure that rendercheck ' 'is installed.') exit(1) else: raise proc = process.communicate()[1].split('\n') found = False tests_pattern = re.compile('.*Available tests: *(.+).*') temp_line = '' tests = [] for line in proc: if found: temp_line += line match = tests_pattern.match(line) if match: first_line = match.group(1).strip().lower() found = True temp_line += first_line for elem in temp_line.split(','): test = elem.strip() if elem: tests.append(test) return tests def main(): usage = 'Usage: %prog [OPTIONS]' all_tests = RenderCheck().get_suites_list() parser = ArgumentParser(usage) parser.add_argument('-i', '--iterations', type=int, default=1, help='The number of times to run the test. \ Default is 1') parser.add_argument('-t', '--test', default='all', help='The name of the test suit to run. \ Available tests: \ %s. \ Default is all' % (', '.join(all_tests))) parser.add_argument('-b', '--blacklist', action='append', help='The name of a test which should not be run.') parser.add_argument('-d', '--debug', action='store_true', help='Choose this to add verbose output \ for debug purposes') parser.add_argument('-o', '--output', default='', help='The path to the log which will be dumped. \ Default is stdout') parser.add_argument('-tp', '--temp', default='', help='The path where to store temporary files. \ Default is /tmp') args = parser.parse_args() # Set up logging to console format = '%(message)s' console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(format)) # Set up the overall logger logger = logging.getLogger() # This is necessary to ensure debug messages are passed through the logger # to the handler logger.setLevel(logging.DEBUG) # This is what happens when -d and/or -o are passed: # -o -> stdout (info) - log (info) # -d -> only stdout (info and debug) - no log # -d -o -> stdout (info) - log (info and debug) # Write to a log if args.output: # Write INFO to stdout console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) # Specify a log file logfile = args.output logfile_handler = logging.FileHandler(logfile) if args.debug: # Write INFO and DEBUG to a log logfile_handler.setLevel(logging.DEBUG) else: # Write INFO to a log logfile_handler.setLevel(logging.INFO) logfile_handler.setFormatter(logging.Formatter(format)) logger.addHandler(logfile_handler) log_path = os.path.abspath(logfile) logging.info("The log can be found at %s" % log_path) # Write only to stdout else: if args.debug: # Write INFO and DEBUG to stdout console_handler.setLevel(logging.DEBUG) logger.addHandler(console_handler) else: # Write INFO to stdout console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) exit_status = 0 if args.test == 'all': tests = all_tests else: tests = args.test.split(',') for test in args.blacklist: if test in tests: tests.remove(test) rendercheck = RenderCheck(args.temp) exit_status = rendercheck.run_test(tests, args.iterations, args.debug) exit(exit_status) if __name__ == '__main__': main() 2013.com.canonical.certification.checkbox-0.4/bin/audio_test0000775000175000017500000006074012320541306023665 0ustar zygazyga00000000000000#!/usr/bin/env python3 from __future__ import division, print_function import argparse import collections import json import logging import math import re import subprocess import sys import time try: import gi gi.require_version('Gst','1.0') from gi.repository import GObject from gi.repository import Gst from gi.repository import GLib Gst.init(None) # This has to be done very early so it can find elements except ImportError: print("Can't import module: %s. it may not be available for this" "version of Python, which is: " % sys.exc_info()[1], file=sys.stderr) print((sys.version), file=sys.stderr) sys.exit(127) #Frequency bands for FFT BINS = 256 #How often to take a sample and do FFT on it. FFT_INTERVAL = 100000000 # In nanoseconds, so this is every 1/10th second #Sampling frequency. The effective maximum frequency we can analyze is #half of this (see Nyquist's theorem) SAMPLING_FREQUENCY = 44100 #The default test frequency is in the middle of the band that contains 5000Hz #This frequency was determined experimentally to be high enough but more #reliable than others we tried. DEFAULT_TEST_FREQUENCY = 5035 #only sample a signal when peak level is in this range (in dB attenuation, #0 means no attenuation (and horrible clipping). REC_LEVEL_RANGE = (-2.0, -12.0) #For our test signal to be considered present, it has to be this much higher #than the base level (minimum magnitude). This is in dB. MAGNITUDE_THRESHOLD = 2.5 #Volume for the sample tone (in %) PLAY_VOLUME = 70 class PIDController(object): """ A Proportional-Integrative-Derivative controller (PID) controls a process's output to try to maintain a desired output value (known as 'setpoint', by continually adjusting the process's input. It does so by calculating the "error" (difference between output and setpoint) and attempting to minimize it manipulating the input. The desired change to the input is calculated based on error and three constants (Kp, Ki and Kd). These values can be interpreted in terms of time: P depends on the present error, I on the accumulation of past errors, and D is a prediction of future errors, based on current rate of change. The weighted sum of these three actions is used to adjust the process via a control element. In practice, Kp, Ki and Kd are process-dependent and usually have to be tweaked by hand, but once reasonable constants are arrived at, they can apply to a particular process without further modification. """ def __init__(self, Kp, Ki, Kd, setpoint=0): """ Creates a PID controller with given constants and setpoint. Arguments: Kp, Ki, Kd: PID constants, see class description. setpoint: desired output value; calls to input_change with a process output reading will return a desired change to the input to attempt matching output to this value. """ self.setpoint = setpoint self.Kp = Kp self.Ki = Ki self.Kd = Kd self._integral = 0 self._previous_error = 0 self._change_limit = 0 def input_change(self, process_feedback, dt): """ Calculates desired input value change. Based on process feedback and time interval (dt). """ error = self.setpoint - process_feedback self._integral = self._integral + (error * dt) derivative = (error - self._previous_error) / dt self._previous_error = error input_change = (self.Kp * error) + \ (self.Ki * self._integral) + \ (self.Kd * derivative) if self._change_limit and abs(input_change) > abs(self._change_limit): sign = input_change / abs(input_change) input_change = sign * self._change_limit return input_change def set_change_limit(self, limit): """Ensures that input value changes are lower than limit. Setting limit of zero disables this. """ self._change_limit = limit class PAVolumeController(object): pa_types = {'input': 'source', 'output': 'sink'} def __init__(self, type, method=None, logger=None): """Initializes the volume controller. Arguments: type: either input or output method: a method that will run a command and return pulseaudio information in the described format, as a single string with line breaks (to be processed with str.splitlines()) """ self.type = type self._volume = None self.identifier = None self.method = method if not isinstance(method, collections.Callable): self.method = self._pactl_output self.logger = logger def set_volume(self, volume): if not 0 <= volume <= 100: return False if not self.identifier: return False command = ['pactl', 'set-%s-volume' % (self.pa_types[self.type]), str(self.identifier[0]), str(int(volume)) + "%"] if False == self.method(command): return False self._volume = volume return True def get_volume(self): if not self.identifier: return None return self._volume def mute(self, mute): mute = str(int(mute)) if not self.identifier: return False command = ['pactl', 'set-%s-mute' % (self.pa_types[self.type]), str(self.identifier[0]), mute] if False == self.method(command): return False return True def get_identifier(self): if self.type: self.identifier = self._get_identifier_for(self.type) if self.identifier and self.logger: message = "Using PulseAudio identifier %s (%s) for %s" %\ (self.identifier + (self.type,)) self.logger.info(message) return self.identifier def _get_identifier_for(self, type): """Gets default PulseAudio identifier for given type. Arguments: type: either input or output Returns: A tuple: (pa_id, pa_description) """ if type not in self.pa_types: return None command = ['pactl', 'list', self.pa_types[type] + "s", 'short'] #Expect lines of this form (field separator is tab): #\t\t\t\t #What we need to return is the ID for the first element on this list #that does not contain auto_null or monitor. pa_info = self.method(command) valid_elements = None if pa_info: reject_regex = '.*(monitor|auto_null).*' valid_elements = [element for element in pa_info.splitlines() if not re.match(reject_regex, element)] if not valid_elements: if self.logger: self.logger.error("No valid PulseAudio elements" " for %s" % (self.type)) return None #We only need the pulseaudio numeric ID and long name for each element valid_elements = [(int(e.split()[0]), e.split()[1]) for e in valid_elements] return valid_elements[0] def _pactl_output(self, command): #This method mainly calls pactl (hence the name). Since pactl may #return a failure if the audio layer is not yet initialized, we will #try running a few times in case of failure. All our invocations of #pactl should be "idempotent" so repeating them should not have #any bad effects. for attempt in range(0, 3): try: return subprocess.check_output(command, universal_newlines=True) except (subprocess.CalledProcessError): time.sleep(5) return False class FileDumper(object): def write_to_file(self, filename, data): try: with open(filename, "w") as f: for i in data: print(i, file=f) return_value = True except (TypeError, IOError): return_value = False return return_value class SpectrumAnalyzer(object): def __init__(self, points, sampling_frequency=44100, wanted_samples=50): self.spectrum = [0] * points self.number_of_samples = 0 self.wanted_samples = wanted_samples self.sampling_frequency = sampling_frequency #Frequencies should contain *real* frequency which is half of #the sampling frequency self.frequencies = [((sampling_frequency / 2.0) / points) * i for i in range(points)] def _average(self): return sum(self.spectrum) / len(self.spectrum) def sample(self, sample): if len(sample) != len(self.spectrum): return self.spectrum = [((old * self.number_of_samples) + new) / (self.number_of_samples + 1) for old, new in zip(self.spectrum, sample)] self.number_of_samples += 1 def frequencies_with_peak_magnitude(self, threshold=1.0): #First establish the base level per_magnitude_bins = collections.defaultdict(int) for magnitude in self.spectrum: per_magnitude_bins[magnitude] += 1 base_level = max(per_magnitude_bins, key=lambda x: per_magnitude_bins[x]) #Now return all values that are higher (more positive) #than base_level + threshold peaks = [] for i in range(1, len(self.spectrum) - 1): first_index = i - 1 last_index = i + 1 if self.spectrum[first_index] < self.spectrum[i] and \ self.spectrum[last_index] < self.spectrum[i] and \ self.spectrum[i] > base_level + threshold: peaks.append(i) return peaks def frequency_band_for(self, frequency): """Convenience function to tell me which band a frequency is contained in """ #Note that actual frequencies are half of what the sampling #frequency would tell us. If SF is 44100 then maximum actual #frequency is 22050, and if I have 10 frequency bins each will #contain only 2205 Hz, not 4410 Hz. max_frequency = self.sampling_frequency / 2 if frequency > max_frequency or frequency < 0: return None band = float(frequency) / (max_frequency / len(self.spectrum)) return int(math.ceil(band)) - 1 def frequencies_for_band(self, band): """Convenience function to tell me the delimiting frequencies for a band """ if band >= len(self.spectrum) or band < 0: return None lower = self.frequencies[band] upper = lower + ((self.sampling_frequency / 2.0) / len(self.spectrum)) return (lower, upper) def sampling_complete(self): return self.number_of_samples >= self.wanted_samples class GStreamerMessageHandler(object): def __init__(self, rec_level_range, logger, volumecontroller, pidcontroller, spectrum_analyzer): """Initializes the message handler. It knows how to handle spectrum and level gstreamer messages. Arguments: rec_level_range: tuple with acceptable recording level ranges logger: logging object with debug, info, error methods. volumecontroller: an instance of VolumeController to use to adjust RECORDING level pidcontroller: a PID controller instance which helps control volume spectrum_analyzer: instance of SpectrumAnalyzer to collect data from spectrum messages """ self.current_level = sys.maxsize self.logger = logger self.pid_controller = pidcontroller self.rec_level_range = rec_level_range self.spectrum_analyzer = spectrum_analyzer self.volume_controller = volumecontroller def set_quit_method(self, method): """ Method that will be called when sampling is complete.""" self._quit_method = method def bus_message_handler(self, bus, message): if message.type == Gst.MessageType.ELEMENT: message_name = message.get_structure().get_name() if message_name == 'spectrum': #TODO: Due to an upstream bug, a structure's get_value method #doesn't work if the value in question is an array (as is the #case with the magnitudes). #https://bugzilla.gnome.org/show_bug.cgi?id=693168 #We have to resort to parsing the string representation of the #structure. It's an ugly hack but it works. #Ideally we'd be able to say this to get fft_magnitudes: #message.get_structure.get_value('magnitude'). #If an upstream fix ever makes it into gstreamer, #remember to remove this hack and the parse_spectrum #method struct_string = message.get_structure().to_string() structure = parse_spectrum_message_structure(struct_string) fft_magnitudes = structure['magnitude'] self.spectrum_method(self.spectrum_analyzer, fft_magnitudes) if message_name == 'level': #peak_value is our process feedback #It's returned as an array, so I need the first (and only) #element peak_value = message.get_structure().get_value('peak')[0] self.level_method(peak_value, self.pid_controller, self.volume_controller) #Adjust recording level def level_method(self, level, pid_controller, volume_controller): #If volume controller doesn't return a valid volume, #we can't control it :( current_volume = volume_controller.get_volume() if current_volume is None: self.logger.error("Unable to control recording volume." "Test results may be wrong") return self.current_level = level change = pid_controller.input_change(level, 0.10) if self.logger: self.logger.debug("Peak level: %(peak_level).2f, " "volume: %(volume)d%%, Volume change: %(change)f%%" % {'peak_level': level, 'change': change, 'volume': current_volume}) volume_controller.set_volume(current_volume + change) #Only sample if level is within the threshold def spectrum_method(self, analyzer, spectrum): if self.rec_level_range[1] <= self.current_level \ or self.current_level <= self.rec_level_range[0]: self.logger.debug("Sampling, recorded %d samples" % analyzer.number_of_samples) analyzer.sample(spectrum) if analyzer.sampling_complete() and self._quit_method: self.logger.info("Sampling complete, ending process") self._quit_method() class GstAudioObject(object): def __init__(self): self.class_name = self.__class__.__name__ def _set_state(self, state, description): self.pipeline.set_state(state) message = "%s: %s" % (self.class_name, description) if self.logger: self.logger.info(message) def start(self): self._set_state(Gst.State.PLAYING, "Starting") def stop(self): self._set_state(Gst.State.NULL, "Stopping") class Player(GstAudioObject): def __init__(self, frequency=DEFAULT_TEST_FREQUENCY, logger=None): super(Player, self).__init__() self.pipeline_description = ("audiotestsrc wave=sine freq=%s " "! audioconvert " "! audioresample " "! autoaudiosink" % int(frequency)) self.logger = logger if self.logger: self.logger.debug(self.pipeline_description) self.pipeline = Gst.parse_launch(self.pipeline_description) class Recorder(GstAudioObject): def __init__(self, output_file, bins=BINS, sampling_frequency=SAMPLING_FREQUENCY, fft_interval=FFT_INTERVAL, logger=None): super(Recorder, self).__init__() pipeline_description = ('''autoaudiosrc ! queue ! level message=true ! audioconvert ! audio/x-raw, channels=1, rate=(int)%(rate)s ! audioresample ! spectrum interval=%(fft_interval)s bands = %(bands)s ! wavenc ! filesink location=%(file)s''' % {'bands': bins, 'rate': sampling_frequency, 'fft_interval': fft_interval, 'file': output_file}) self.logger = logger if self.logger: self.logger.debug(pipeline_description) self.pipeline = Gst.parse_launch(pipeline_description) def register_message_handler(self, handler_method): if self.logger: message = "Registering message handler: %s" % handler_method self.logger.debug(message) self.bus = self.pipeline.get_bus() self.bus.add_signal_watch() self.bus.connect('message', handler_method) def parse_spectrum_message_structure(struct_string): #First let's jsonize this #This is the message name, which we don't need text = struct_string.replace("spectrum, ", "") #name/value separator in json is : and not = text = text.replace("=",": ") #Mutate the {} array notation from the structure to #[] notation for json. text = text.replace("{","[") text = text.replace("}","]") #Remove a few stray semicolons that aren't needed text = text.replace(";","") #Remove the data type fields, as json doesn't need them text = re.sub(r"\(.+?\)", "", text) #double-quote the identifiers text = re.sub(r"([\w-]+):", r'"\1":', text) #Wrap the whole thing in brackets text = ("{"+text+"}") #Try to parse and return something sensible here, even if #the data was unparsable. try: return json.loads(text) except ValueError: return None def process_arguments(): description = """ Plays a single frequency through the default output, then records on the default input device. Analyzes the recorded signal to test for presence of the played frequency, if present it exits with success. """ parser = argparse.ArgumentParser(description=description) parser.add_argument("-t", "--time", dest='test_duration', action='store', default=30, type=int, help="""Maximum test duration, default %(default)s seconds. It may exit sooner if it determines it has enough data.""") parser.add_argument("-a", "--audio", action='store', default="/dev/null", type=str, help="File to save recorded audio in .wav format") parser.add_argument("-q", "--quiet", action='store_true', default=False, help="Be quiet, no output unless there's an error.") parser.add_argument("-d", "--debug", action='store_true', default=False, help="Debugging output") parser.add_argument("-f", "--frequency", action='store', default=DEFAULT_TEST_FREQUENCY, type=int, help="Frequency for test signal, default %(default)s Hz") parser.add_argument("-u", "--spectrum", action='store', type=str, help="""File to save spectrum information for plotting (one frequency/magnitude pair per line)""") return parser.parse_args() # def main(): #Get arguments. args = process_arguments() #Setup logging level = logging.INFO if args.debug: level = logging.DEBUG if args.quiet: level = logging.ERROR logging.basicConfig(level=level) try: #Launches recording pipeline. I need to hook up into the gst #messages. recorder = Recorder(output_file=args.audio, logger=logging) #Just launches the playing pipeline player = Player(frequency=args.frequency, logger=logging) except GObject.GError as excp: logging.critical("Unable to initialize GStreamer pipelines: %s", excp) sys.exit(127) #This just receives a process feedback and tells me how much to change to #achieve the setpoint pidctrl = PIDController(Kp=0.7, Ki=.01, Kd=0.01, setpoint=REC_LEVEL_RANGE[0]) pidctrl.set_change_limit(5) #This gathers spectrum data. analyzer = SpectrumAnalyzer(points=BINS, sampling_frequency=SAMPLING_FREQUENCY) #Volume controllers actually set volumes for their device types. #we should at least issue a warning recorder.volumecontroller = PAVolumeController(type='input', logger=logging) if not recorder.volumecontroller.get_identifier(): logging.warning("Unable to get input volume control identifier. " "Test results will probably be invalid") recorder.volumecontroller.set_volume(0) recorder.volumecontroller.mute(False) player.volumecontroller = PAVolumeController(type='output', logger=logging) if not player.volumecontroller.get_identifier(): logging.warning("Unable to get output volume control identifier. " "Test results will probably be invalid") player.volumecontroller.set_volume(PLAY_VOLUME) player.volumecontroller.mute(False) #This handles the messages from gstreamer and orchestrates #the passed volume controllers, pid controller and spectrum analyzer #accordingly. gmh = GStreamerMessageHandler(rec_level_range=REC_LEVEL_RANGE, logger=logging, volumecontroller=recorder.volumecontroller, pidcontroller=pidctrl, spectrum_analyzer=analyzer) #I need to tell the recorder which method will handle messages. recorder.register_message_handler(gmh.bus_message_handler) #Create the loop and add a few triggers # GObject.threads_init() #Not needed? loop = GLib.MainLoop() GLib.timeout_add_seconds(0, player.start) GLib.timeout_add_seconds(0, recorder.start) GLib.timeout_add_seconds(args.test_duration, loop.quit) # Tell the gmh which method to call when enough samples are collected gmh.set_quit_method(loop.quit) loop.run() #When the loop ends, set things back to reasonable states player.stop() recorder.stop() player.volumecontroller.set_volume(50) recorder.volumecontroller.set_volume(10) #See if data gathering was successful. test_band = analyzer.frequency_band_for(args.frequency) candidate_bands = analyzer.frequencies_with_peak_magnitude(MAGNITUDE_THRESHOLD) for band in candidate_bands: logging.debug("Band (%.2f,%.2f) contains a magnitude peak" % analyzer.frequencies_for_band(band)) if test_band in candidate_bands: freqs_for_band = analyzer.frequencies_for_band(test_band) logging.info("PASS: Test frequency of %s in band (%.2f, %.2f) " "which contains a magnitude peak" % ((args.frequency,) + freqs_for_band)) return_value = 0 else: logging.info("FAIL: Test frequency of %s is not in one of the " "bands with magnitude peaks" % args.frequency) return_value = 1 #Is the microphone broken? if len(set(analyzer.spectrum)) <= 1: logging.info("WARNING: Microphone seems broken, didn't even " "record ambient noise") if args.spectrum: logging.info("Saving spectrum data for plotting as %s" % args.spectrum) if not FileDumper().write_to_file(args.spectrum, ["%s,%s" % t for t in zip(analyzer.frequencies, analyzer.spectrum)]): logging.error("Couldn't save spectrum data for plotting", file=sys.stderr) return return_value if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network_ntp_test0000775000175000017500000001610512320541306025132 0ustar zygazyga00000000000000#!/usr/bin/env python3 ''' Program to test that ntpdate will sync the clock with an internet time server. Copyright (C) 2010 Canonical Ltd. Author: Jeff Lane This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . The purpose of this script is to test to see whether the test system can connect to an internet time server and sync the local clock. It will also check to see if ntpd is running locally, and if so, stop it for the duration of the test and restart it after the test is finished. By default, we're hitting ntp.ubuntu.com, however you can use any valid NTP server by passing the URL to the program via --server ''' import sys import os import logging import signal import time from datetime import datetime, timedelta from subprocess import Popen, PIPE from argparse import ArgumentParser def SilentCall(*popenargs): ''' Modified version of subprocess.call() to supress output from the command that is executed. Wait for command to complete, then return the returncode attribute. ''' null_fh = open('/dev/null', 'wb', 0) try: return (Popen(*popenargs, shell=True, stdout=null_fh, stderr=null_fh) .wait()) finally: null_fh.close() def CheckNTPD(): ''' This checks to see if nptd is running or not, if so it returns a tuple (status,pid,command) where status is either on or off. ''' ps_list = (Popen('ps axo pid,comm', shell=True, stdout=PIPE) .communicate()[0].splitlines()) for item in ps_list: fields = item.split() if fields[1] == 'ntpd': logging.debug('Found %s with PID %s' % (fields[1], fields[0])) break if fields[1] == 'ntpd': return ('on', fields[0], fields[1]) else: return ('off', '0', '0') def StartStopNTPD(state, pid=0): ''' This is used to either start or stop ntpd if its running. ''' if state == 'off': logging.info('Stopping ntpd process PID: %s' % pid) os.kill(int(pid), signal.SIGHUP) elif state == 'on': logging.info('Starting ntp process') SilentCall('/etc/init.d/ntp start') ntpd_status = CheckNTPD() if status == 0: logging.debug('ntpd restarted with PID: %s' % ntpd_status[1]) else: logging.error('ntpd restart failed for some reason') else: logging.error('%s is an unknown state, unable to start/stop ntpd' % state) def SyncTime(server): ''' This is used to sync time to the specified ntp server. We use -b here as that syncs time faster than the slewed method that ntpdate uses by default, meaning we'll see something meaningful faster. ''' cmd = 'ntpdate -b ' + server logging.debug('using %s' % server) sync = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) result = sync.communicate() if sync.returncode == 0: logging.info('Successful NTP update from %s' % server) logging.debug(result[0].strip().decode()) return True else: logging.error('Failed to sync with %s' % server) logging.error(result[1].strip().decode()) return False def TimeCheck(): ''' Returns current time in a time.localtime() struct ''' return time.localtime() def SkewTime(): ''' Optional function. We can skew time by 1 hour if we'd like to see real sync changes being enforced ''' TIME_SKEW = 1 logging.info('Time Skewing has been selected. Setting clock ahead 1 hour') # Let's get our current time skewed = datetime.now() + timedelta(hours=TIME_SKEW) logging.info('Current time is: %s' % time.asctime()) # Now create new time string in the form MMDDhhmmYYYY for the date program date_time_string = skewed.strftime('%m%d%H%M%Y') logging.debug('New date string is: %s' % date_time_string) logging.debug('Setting new system time/date') # This call is necessary for testing, otherwise TimeSkew() does nothing. SilentCall('/bin/date %s' % date_time_string) logging.info('Pre-sync time is: %s' % time.asctime()) def main(): description = 'Tests the ability to skew and sync the clock with an NTP server' parser = ArgumentParser(description=description) parser.add_argument('--server', action='store', default='ntp.ubuntu.com', help='The NTP server to sync from. The default server \ is %(default)s') parser.add_argument('--skew-time', action='store_true', default=False, help='Setting this will change system time ahead by 1 \ hour to make the results of ntp syncing more dramatic \ and noticeable.') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Verbose output for debugging purposes') args = parser.parse_args() if os.geteuid() != 0: parser.error("You must run this script as root") # Set up logging format = '%(asctime)s %(levelname)-8s %(message)s' date_format = '%Y-%m-%d %H:%M:%S' handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(format, date_format)) logger = logging.getLogger() logger.setLevel(logging.INFO) if args.debug: logger.setLevel(logging.DEBUG) logger.addHandler(handler) # Make sure NTP is installed if not os.access('/usr/sbin/ntpdate', os.F_OK): logging.error('NTP is not installed!') return 1 # Check for and stop the ntp daemon ntpd_status = CheckNTPD() logging.debug('Pre-sync ntpd status: %s %s %s' % (ntpd_status[0], ntpd_status[1], ntpd_status[2])) if ntpd_status[0] == 'on': logging.debug('Since ntpd is currently running, stopping it now') StartStopNTPD('off', ntpd_status[1]) if args.skew_time: logging.debug('Setting system time ahead one hour') SkewTime() else: logging.info('Pre-sync time is: %s' % time.asctime(TimeCheck())) sync = SyncTime(args.server) logging.info('Current system time is: %s' % time.asctime(TimeCheck())) # Restart ntp daemon if ntpd_status[0] == 'on': logging.debug('Since ntpd was previously running, starting it again') StartStopNTPD('on') if sync is True: return 0 else: return 1 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/optical_write_test0000775000175000017500000000771112320541306025430 0ustar zygazyga00000000000000#!/bin/bash TEMP_DIR='/tmp/optical-test' ISO_NAME='optical-test.iso' SAMPLE_FILE_PATH='/usr/share/example-content/' SAMPLE_FILE='Ubuntu_Free_Culture_Showcase' MD5SUM_FILE='optical_test.md5' START_DIR=$PWD create_working_dirs(){ # First, create the temp dir and cd there echo "Creating Temp directory and moving there ..." mkdir -p $TEMP_DIR || return 1 cd $TEMP_DIR echo "Now working in $PWD ..." } get_sample_data(){ # Get our sample files echo "Getting sample files from $SAMPLE_FILE_PATH ..." cp -a $SAMPLE_FILE_PATH/$SAMPLE_FILE $TEMP_DIR return $? } generate_md5(){ # Generate the md5sum echo "Generating md5sums of sample files ..." CUR_DIR=$PWD cd $SAMPLE_FILE md5sum * > $TEMP_DIR/$MD5SUM_FILE # Check the sums for paranoia sake check_md5 $TEMP_DIR/$MD5SUM_FILE rt=$? cd $CUR_DIR return $rt } check_md5(){ echo "Checking md5sums ..." md5sum -c $1 return $? } generate_iso(){ # Generate ISO image echo "Creating ISO Image ..." genisoimage -r -J -o $ISO_NAME $SAMPLE_FILE return $? } burn_iso(){ # Burn the ISO with the appropriate tool echo "Sleeping 10 seconds in case drive is not yet ready ..." sleep 10 echo "Beginning image burn ..." if [ "$OPTICAL_TYPE" == 'cd' ] then wodim -eject dev=$OPTICAL_DRIVE $ISO_NAME elif [ "$OPTICAL_TYPE" == 'dvd' ] || [ "$OPTICAL_TYPE" == 'bd' ] then growisofs -dvd-compat -Z $OPTICAL_DRIVE=$ISO_NAME else echo "Invalid type specified '$OPTICAL_TYPE'" exit 1 fi rt=$? return $rt } check_disk(){ TIMEOUT=300 SLEEP_COUNT=0 INTERVAL=3 # Give the tester up to 5 minutes to reload the newly created CD/DVD echo "Waiting up to 5 minutes for drive to be mounted ..." while true; do sleep $INTERVAL SLEEP_COUNT=`expr $SLEEP_COUNT + $INTERVAL` mount $OPTICAL_DRIVE 2>&1 |egrep -q "already mounted" rt=$? if [ $rt -eq 0 ]; then echo "Drive appears to be mounted now" break fi # If they exceed the timeout limit, make a best effort to load the tray # in the next steps if [ $SLEEP_COUNT -ge $TIMEOUT ]; then echo "WARNING: TIMEOUT Exceeded and no mount detected!" break fi done echo "Deleting original data files ..." rm -rf $SAMPLE_FILE if [ -n "$(mount | grep $OPTICAL_DRIVE)" ]; then MOUNT_PT=$(mount | grep $OPTICAL_DRIVE | awk '{print $3}') echo "Disk is mounted to $MOUNT_PT" else echo "Attempting best effort to mount $OPTICAL_DRIVE on my own" MOUNT_PT=$TEMP_DIR/mnt echo "Creating temp mount point: $MOUNT_PT ..." mkdir $MOUNT_PT echo "Mounting disk to mount point ..." mount $OPTICAL_DRIVE $MOUNT_PT rt=$? if [ $rt -ne 0 ]; then echo "ERROR: Unable to re-mount $OPTICAL_DRIVE!" >&2 return 1 fi fi echo "Copying files from ISO ..." cp $MOUNT_PT/* $TEMP_DIR check_md5 $MD5SUM_FILE return $? } cleanup(){ echo "Moving back to original location" cd $START_DIR echo "Now residing in $PWD" echo "Cleaning up ..." umount $MOUNT_PT rm -fr $TEMP_DIR echo "Ejecting spent media ..." eject $OPTICAL_DRIVE } failed(){ echo $1 echo "Attempting to clean up ..." cleanup exit 1 } if [ -e $1 ]; then OPTICAL_DRIVE=$(readlink -f $1) else OPTICAL_DRIVE='/dev/sr0' fi if [ -n "$2" ]; then OPTICAL_TYPE=$2 else OPTICAL_TYPE='cd' fi create_working_dirs || failed "Failed to create working directories" get_sample_data || failed "Failed to copy sample data" generate_md5 || failed "Failed to generate initial md5" generate_iso || failed "Failed to create ISO image" burn_iso || failed "Failed to burn ISO image" check_disk || failed "Failed to verify files on optical disk" cleanup || failed "Failed to clean up" exit 0 2013.com.canonical.certification.checkbox-0.4/bin/frequency_governors_test0000775000175000017500000006420112320541306026665 0ustar zygazyga00000000000000#!/usr/bin/env python3 import decimal import os import re import sys import time import argparse import logging from subprocess import check_output, check_call, CalledProcessError, PIPE class CPUScalingTest(object): def __init__(self): self.speedUpTolerance = 10.0 # percent self.retryLimit = 5 self.retryTolerance = 5.0 # percent self.sysCPUDirectory = "/sys/devices/system/cpu" self.cpufreqDirectory = os.path.join(self.sysCPUDirectory, "cpu0", "cpufreq") self.idaFlag = "ida" self.idaSpeedupFactor = 8.0 # percent self.selectorExe = "cpufreq-selector" self.ifSelectorExe = None def getCPUFreqDirectories(self): logging.debug("Getting CPU Frequency Directories") if not os.path.exists(self.sysCPUDirectory): logging.error("No file %s" % self.sysCPUDirectory) return None # look for cpu subdirectories pattern = re.compile("cpu(?P[0-9]+)") self.cpufreqDirectories = list() for subdirectory in os.listdir(self.sysCPUDirectory): match = pattern.search(subdirectory) if match and match.group("cpuNumber"): cpufreqDirectory = os.path.join(self.sysCPUDirectory, subdirectory, "cpufreq") if not os.path.exists(cpufreqDirectory): logging.error("CPU %s has no cpufreq directory %s" % (match.group("cpuNumber"), cpufreqDirectory)) return None # otherwise self.cpufreqDirectories.append(cpufreqDirectory) if len(self.cpufreqDirectories) is 0: return None # otherwise logging.debug("Located the following CPU Freq Directories:") for line in self.cpufreqDirectories: logging.debug(" %s" % line) return self.cpufreqDirectories def checkParameters(self, file): logging.debug("Checking Parameters for %s" % file) current = None for cpufreqDirectory in self.cpufreqDirectories: parameters = self.getParameters(cpufreqDirectory, file) if not parameters: logging.error("Error: could not determine cpu parameters from %s" % os.path.join(cpufreqDirectory, file)) return None if not current: current = parameters elif not current == parameters: return None return current def getParameters(self, cpufreqDirectory, file): logging.debug("Getting Parameters for %s" % file) path = os.path.join(cpufreqDirectory, file) file = open(path) while 1: line = file.readline() if not line: break if len(line.strip()) > 0: return line.strip().split() return None def setParameter(self, setFile, readFile, value, skip=False, automatch=False): def findParameter(targetFile): logging.debug("Finding parameters for %s" % targetFile) for root, _, files in os.walk(self.sysCPUDirectory): for f in files: rf = os.path.join(root, f) if targetFile in rf: return rf return None logging.debug("Setting %s to %s" % (setFile,value)) path = None if not skip: if automatch: path = findParameter(setFile) else: path = os.path.join(self.cpufreqDirectory, setFile) try: check_call("echo \"%s\" > %s" % (value, path), shell=True) except CalledProcessError as exception: logging.exception("Command failed:") logging.exception(exception) return False # verify it has changed if automatch: path = findParameter(readFile) else: path = os.path.join(self.cpufreqDirectory, readFile) parameterFile = open(path) line = parameterFile.readline() if not line or line.strip() != str(value): logging.error("Error: could not verify that %s was set to %s" % (path, value)) if line: logging.error("Actual Value: %s" % line) else: logging.error("parameter file was empty") return False return True def checkSelectorExecutable(self): logging.debug("Determining if %s is executable" % self.selectorExe) def is_exe(fpath): return os.path.exists(fpath) and os.access(fpath, os.X_OK) if self.ifSelectorExe is None: # cpufreq-selector default path exe = os.path.join("/usr/bin/", self.selectorExe) if is_exe(exe): self.ifSelectorExe = True return True for path in os.environ["PATH"].split(os.pathsep): exe = os.path.join(path, self.selectorExe) if is_exe(exe): self.ifSelectorExe = True return True self.ifSelectorExe = False return False def setParameterWithSelector(self, switch, setFile, readFile, value): logging.debug("Setting %s with %s to %s" % (setFile, switch, value)) # Try the command for all CPUs skip = True if self.checkSelectorExecutable(): try: check_call("cpufreq-selector -%s %s" % (switch, value), shell=True) except CalledProcessError as exception: logging.exception("Note: command failed: %s" % exception.cmd) skip = False else: skip = False return self.setParameter(setFile, readFile, value, skip) def setFrequency(self, frequency): logging.debug("Setting Frequency to %s" % frequency) return self.setParameterWithSelector("f", "scaling_setspeed", "scaling_cur_freq", frequency) def setGovernor(self, governor): logging.debug("Setting Governor to %s" % governor) return self.setParameterWithSelector("g", "scaling_governor", "scaling_governor", governor) def getParameter(self, parameter): value = None logging.debug("Getting %s" % parameter) parameterFilePath = os.path.join(self.cpufreqDirectory, parameter) try: parameterFile = open(parameterFilePath) line = parameterFile.readline() if not line: logging.error("Error: failed to get %s for %s" % (parameter, self.cpufreqDirectory)) return None value = line.strip() return value except IOError as exception: logging.exception("Error: could not open %s" % parameterFilePath) logging.exception(exception) return None def getParameterList(self, parameter): logging.debug("Getting parameter list") values = list() for cpufreqDirectory in self.cpufreqDirectories: path = os.path.join(cpufreqDirectory, parameter) parameterFile = open(path) line = parameterFile.readline() if not line: logging.error("Error: failed to get %s for %s" % (parameter, cpufreqDirectory)) return None values.append(line.strip()) logging.debug("Found parameters:") for line in values: logging.debug(" %s" % line) return values def runLoadTest(self): logging.info("Running CPU load test...") try: output = check_output("taskset -pc 0 %s" % os.getpid(), shell=True) for line in output.decode().splitlines(): logging.info(line) except CalledProcessError as exception: logging.exception("Could not set task affinity") logging.exception(exception) return None runTime = None tries = 0 while tries < self.retryLimit: sys.stdout.flush() (start_utime, start_stime, start_cutime, start_cstime, start_elapsed_time) = os.times() self.pi() (stop_utime, stop_stime, stop_cutime, stop_cstime, stop_elapsed_time) = os.times() if not runTime: runTime = stop_elapsed_time - start_elapsed_time else: thisTime = stop_elapsed_time - start_elapsed_time if ((abs(thisTime - runTime) / runTime) * 100 < self.retryTolerance): return runTime else: runTime = thisTime tries += 1 logging.error("Could not repeat load test times within %.1f%%" % self.retryTolerance) return None def pi(self): decimal.getcontext().prec = 500 s = decimal.Decimal(1) h = decimal.Decimal(3).sqrt() / 2 n = 6 for i in range(170): s2 = ((1 - h) ** 2 + s ** 2 / 4) s = s2.sqrt() h = (1 - s2 / 4).sqrt() n = 2 * n return True def verifyMinimumFrequency(self, waitTime=5): logging.debug("Verifying minimum frequency") logging.info("Waiting %d seconds..." % waitTime) time.sleep(waitTime) logging.info("Done.") minimumFrequency = self.getParameter("scaling_min_freq") currentFrequency = self.getParameter("scaling_cur_freq") if (not minimumFrequency or not currentFrequency or (minimumFrequency != currentFrequency)): return False # otherwise return True def getSystemCapabilities(self): logging.info("System Capabilites:") logging.info("-------------------------------------------------") # Do the CPUs support scaling? if not self.getCPUFreqDirectories(): return False if len(self.cpufreqDirectories) > 1: logging.info("System has %u cpus" % len(self.cpufreqDirectories)) # Ensure all CPUs support the same frequencies freqFileName = "scaling_available_frequencies" self.frequencies = self.checkParameters(freqFileName) if not self.frequencies: return False logging.info("Supported CPU Frequencies: ") for freq in self.frequencies: f = int(freq) / 1000 logging.info(" %u MHz" % f) # Check governors to verify all CPUs support the same control methods governorFileName = "scaling_available_governors" self.governors = self.checkParameters(governorFileName) if not self.governors: return False logging.info("Supported Governors: ") for governor in self.governors: logging.info(" %s" % governor) self.originalGovernors = self.getParameterList("scaling_governor") if self.originalGovernors: logging.info("Current governors:") i = 0 for g in self.originalGovernors: logging.info(" cpu%u: %s" % (i, g)) i += 1 else: logging.error("Error: could not determine current governor settings") return False self.getCPUFlags() return True def getCPUFlags(self): logging.debug("Getting CPU flags") self.cpuFlags = None try: cpuinfo_file = open('/proc/cpuinfo', 'r') cpuinfo = cpuinfo_file.read().split("\n") cpuinfo_file.close() for line in cpuinfo: if line.startswith('flags'): pre, post = line.split(':') self.cpuFlags = post.strip().split() break logging.debug("Found the following CPU Flags:") for line in self.cpuFlags: logging.debug(" %s" % line) except: logging.warning("Could not read CPU flags") def runUserSpaceTests(self): logging.info("Userspace Governor Test:") logging.info("-------------------------------------------------") self.minimumFrequencyTestTime = None self.maximumFrequencyTestTime = None success = True differenceSpeedUp = None governor = "userspace" if governor not in self.governors: logging.warning("Note: %s governor not supported" % governor) else: # Set the governor to "userspace" and verify logging.info("Setting governor to %s" % governor) if not self.setGovernor(governor): success = False # Set the the CPU speed to it's lowest value frequency = self.frequencies[-1] logging.info("Setting CPU frequency to %u MHz" % (int(frequency) / 1000)) if not self.setFrequency(frequency): success = False # Verify the speed is set to the lowest value minimumFrequency = self.getParameter("scaling_min_freq") currentFrequency = self.getParameter("scaling_cur_freq") if (not minimumFrequency or not currentFrequency or (minimumFrequency != currentFrequency)): logging.error("Could not verify that cpu frequency is set to the minimum value of %s" % minimumFrequency) success = False # Run Load Test self.minimumFrequencyTestTime = self.runLoadTest() if not self.minimumFrequencyTestTime: logging.error("Could not retrieve the minimum frequency test's execution time.") success = False else: logging.info("Minimum frequency load test time: %.2f" % self.minimumFrequencyTestTime) # Set the CPU speed to it's highest value as above. frequency = self.frequencies[0] logging.info("Setting CPU frequency to %u MHz" % (int(frequency) / 1000)) if not self.setFrequency(frequency): success = False maximumFrequency = self.getParameter("scaling_max_freq") currentFrequency = self.getParameter("scaling_cur_freq") if (not maximumFrequency or not currentFrequency or (maximumFrequency != currentFrequency)): logging.error("Could not verify that cpu frequency is set to the maximum value of %s" % maximumFrequency) success = False # Repeat workload test self.maximumFrequencyTestTime = self.runLoadTest() if not self.maximumFrequencyTestTime: logging.error("Could not retrieve the maximum frequency test's execution time.") success = False else: logging.info("Maximum frequency load test time: %.2f" % self.maximumFrequencyTestTime) # Verify MHz increase is comparable to time % decrease predictedSpeedup = (float(maximumFrequency) / float(minimumFrequency)) # If "ida" turbo thing, increase the expectation by 8% if self.cpuFlags and self.idaFlag in self.cpuFlags: logging.info("Found %s flag, increasing expected speedup by %.1f%%" % (self.idaFlag, self.idaSpeedupFactor)) predictedSpeedup = \ (predictedSpeedup * (1.0 / (1.0 - (self.idaSpeedupFactor / 100.0)))) if self.minimumFrequencyTestTime and self.maximumFrequencyTestTime: measuredSpeedup = (self.minimumFrequencyTestTime / self.maximumFrequencyTestTime) logging.info("CPU Frequency Speed Up: %.2f" % predictedSpeedup) logging.info("Measured Speed Up: %.2f" % measuredSpeedup) differenceSpeedUp = (((measuredSpeedup - predictedSpeedup) / predictedSpeedup) * 100) logging.info("Percentage Difference %.1f%%" % differenceSpeedUp) if differenceSpeedUp > self.speedUpTolerance: logging.error("Measured speedup vs expected speedup is %.1f%% and is not within %.1f%% margin." % (differenceSpeedUp, self.speedUpTolerance)) success = False elif differenceSpeedUp < 0: logging.info(""" Measured speed up %.2f exceeded expected speedup %.2f """ % (measuredSpeedup, predictedSpeedup)) else: logging.error("Not enough timing data to calculate speed differences.") return success def runOnDemandTests(self): logging.info("On Demand Governor Test:") logging.info("-------------------------------------------------") differenceOnDemandVsMaximum = None onDemandTestTime = None governor = "ondemand" success = True if governor not in self.governors: logging.warning("%s governor not supported" % governor) else: # Set the governor to "ondemand" logging.info("Setting governor to %s" % governor) if not self.setGovernor(governor): success = False # Wait a fixed period of time, then verify current speed # is the slowest in as before if not self.verifyMinimumFrequency(): logging.error("Could not verify that cpu frequency has settled to the minimum value") success = False # Repeat workload test onDemandTestTime = self.runLoadTest() if not onDemandTestTime: logging.warning("No On Demand load test time available.") success = False else: logging.info("On Demand load test time: %.2f" % onDemandTestTime) if onDemandTestTime and self.maximumFrequencyTestTime: # Compare the timing to the max results from earlier, # again time should be within self.speedUpTolerance differenceOnDemandVsMaximum = \ (abs(onDemandTestTime - self.maximumFrequencyTestTime) / self.maximumFrequencyTestTime) * 100 logging.info("Percentage Difference vs. maximum frequency: %.1f%%" % differenceOnDemandVsMaximum) if differenceOnDemandVsMaximum > self.speedUpTolerance: logging.error("On demand performance vs maximum of %.1f%% is not within %.1f%% margin" % (differenceOnDemandVsMaximum, self.speedUpTolerance)) success = False else: logging.error("Not enough timing data to calculate speed differences.") # Verify the current speed has returned to the lowest speed again if not self.verifyMinimumFrequency(): logging.error("Could not verify that cpu frequency has settled to the minimum value") success = False return success def runPerformanceTests(self): logging.info("Performance Governor Test:") logging.info("-------------------------------------------------") differencePerformanceVsMaximum = None governor = "performance" success = True if governor not in self.governors: logging.warning("%s governor not supported" % governor) else: # Set the governor to "performance" logging.info("Setting governor to %s" % governor) if not self.setGovernor(governor): success = False # Verify the current speed is the same as scaling_max_freq maximumFrequency = self.getParameter("scaling_max_freq") currentFrequency = self.getParameter("scaling_cur_freq") if (not maximumFrequency or not currentFrequency or (maximumFrequency != currentFrequency)): logging.error("Current cpu frequency of %s is not set to the maximum value of %s" % (currentFrequency, maximumFrequency)) success = False # Repeat work load test performanceTestTime = self.runLoadTest() if not performanceTestTime: logging.error("No Performance load test time available.") success = False else: logging.info("Performance load test time: %.2f" % performanceTestTime) if performanceTestTime and self.maximumFrequencyTestTime: # Compare the timing to the max results differencePerformanceVsMaximum = \ (abs(performanceTestTime - self.maximumFrequencyTestTime) / self.maximumFrequencyTestTime) * 100 logging.info("Percentage Difference vs. maximum frequency: %.1f%%" % differencePerformanceVsMaximum) if differencePerformanceVsMaximum > self.speedUpTolerance: logging.error("Performance setting vs maximum of %.1f%% is not within %.1f%% margin" % (differencePerformanceVsMaximum, self.speedUpTolerance)) success = False else: logging.error("Not enough timing data to calculate speed differences.") return success def runConservativeTests(self): logging.info("Conservative Governor Test:") logging.info("-------------------------------------------------") differenceConservativeVsMinimum = None governor = "conservative" success = True if governor not in self.governors: logging.warning("%s governor not supported" % governor) else: # Set the governor to "conservative" logging.info("Setting governor to %s" % governor) if not self.setGovernor(governor): success = False # Set the frequency step to 20, # so that it jumps to minimum frequency path = os.path.join("conservative", "freq_step") if not self.setParameter(path, path, 20, automatch=True): success = False # Wait a fixed period of time, # then verify current speed is the slowest in as before if not self.verifyMinimumFrequency(10): logging.error("Could not verify that cpu frequency has settled to the minimum value") success = False # Set the frequency step to 0, # so that it doesn't gradually increase if not self.setParameter(path, path, 0, automatch=True): success = False # Repeat work load test conservativeTestTime = self.runLoadTest() if not conservativeTestTime: logging.error("No Conservative load test time available.") success = False else: logging.info("Conservative load test time: %.2f" % conservativeTestTime) if conservativeTestTime and self.minimumFrequencyTestTime: # Compare the timing to the max results differenceConservativeVsMinimum = \ (abs(conservativeTestTime - self.minimumFrequencyTestTime) / self.minimumFrequencyTestTime) * 100 logging.info("Percentage Difference vs. minimum frequency: %.1f%%" % differenceConservativeVsMinimum) if differenceConservativeVsMinimum > self.speedUpTolerance: logging.error("Performance setting vs minimum of %.1f%% is not within %.1f%% margin" % (differenceConservativeVsMinimum, self.speedUpTolerance)) success = False else: logging.error("Not enough timing data to calculate speed differences.") return success def restoreGovernors(self): logging.info("Restoring original governor to %s" % (self.originalGovernors[0])) self.setGovernor(self.originalGovernors[0]) def main(): parser = argparse.ArgumentParser() parser.add_argument("-q", "--quiet", action="store_true", help="Suppress output.") parser.add_argument("-c", "--capabilities", action="store_true", help="Only output CPU capabilities.") parser.add_argument("-d", "--debug", action="store_true", help="Turn on debug level output for extra info during test run.") args = parser.parse_args() # Set up the logging system (unless we don't want ANY logging) if not args.quiet or args.debug: logger = logging.getLogger() logger.setLevel(logging.DEBUG) format = '%(asctime)s %(levelname)-8s %(message)s' date_format = '%Y-%m-%d %H:%M:%S' # If we DO want console output if not args.quiet: console_handler = logging.StreamHandler() #logging.StreamHandler() console_handler.setFormatter(logging.Formatter(format,date_format)) console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) # If we ALSO want to create a file for future reference if args.debug: console_handler.setLevel(logging.DEBUG) test = CPUScalingTest() if not os.path.exists(test.cpufreqDirectory): logging.info("CPU Frequency Scaling not supported") return 0 if not test.getSystemCapabilities(): logging.error("Failed to get system capabilities") return 1 returnValues = [] if not args.capabilities: logging.info("Beginning Frequency Governors Testing") returnValues.append(test.runUserSpaceTests()) returnValues.append(test.runOnDemandTests()) returnValues.append(test.runPerformanceTests()) returnValues.append(test.runConservativeTests()) test.restoreGovernors() return 1 if False in returnValues else 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/battery_test0000775000175000017500000001437412320541306024240 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import time import re import subprocess import sys import argparse from gi.repository import Gio class Battery(): def __init__(self, data): lines = data.split("\n") for line in lines: if line.find("state:") != -1: self._state = line.split(':')[1].strip() elif line.find("energy:") != -1: self._energy, self._energy_units = self._get_capacity(line) elif line.find("energy-full:") != -1: self._energy_full, self._energy_full_units =\ self._get_capacity(line) elif line.find("energy-full-design:") != -1: self._energy_full_design, self._energy_full_design_units =\ self._get_capacity(line) def _get_capacity(self, line): """ Given a line of input that represents a battery capacity (energy) value, return a tuple of (value, units). Value is returned as a float. """ capacity = line.split(':')[1].strip() values = capacity.split() return (float(values[0]), values[1]) def __str__(self): ret = "-----------------------------------------\n" ret += "State: %s\n" % self._state ret += "Energy: %s %s\n" % (self._energy, self._energy_units) ret += "Energy Full: %s %s\n" % (self._energy_full, self._energy_full_units) ret += "Energy Full-Design: %s %s\n" % (self._energy_full_design, self._energy_full_design_units) return ret def find_battery(): batinfo = subprocess.Popen('upower -d', stdout=subprocess.PIPE, shell=True, universal_newlines=True) if not batinfo: return None else: out, err = batinfo.communicate() if out: device_regex = re.compile("Device: (.*battery_.*)") batteries = device_regex.findall(out) if len(batteries) == 0: return None elif len(batteries) > 1: print("Warning: This system has more than 1 battery, only the" "first battery will be measured") return batteries[0] else: return None def get_battery_state(): battery_name = find_battery() if battery_name is None: return None batinfo = subprocess.Popen('upower -i %s' % battery_name, stdout=subprocess.PIPE, shell=True, universal_newlines=True) if not batinfo: return None else: out, err = batinfo.communicate() if out: return Battery(out) else: return None def validate_battery_info(battery): if battery is None: print ("Error obtaining battery info") return False if battery._state != "discharging": print ("Error: battery is not discharging, test will not be valid") return False return True def battery_life(before, after, time): capacity_difference = before._energy - after._energy print("Battery drained by %f %s" % (capacity_difference, before._energy_units)) if capacity_difference == 0: print("Battery capacity did not change, unable to determine remaining" " time", file=sys.stderr) return 1 drain_per_second = capacity_difference / time print("Battery drained %f %s per second" % (drain_per_second, before._energy_units)) # the battery at it's max design capacity (when it was brand new) design_life_minutes = round( ((before._energy_full_design / drain_per_second) / 60), 2) print("Battery Life with full battery at design capacity (when new): %.2f" "minutes" % (design_life_minutes)) # the battery at it's current max capacity current_full_life_minutes = round( ((before._energy_full / drain_per_second) / 60), 2) print("Battery Life with a full battery at current capacity: %.2f minutes" % (current_full_life_minutes)) # the battery at it's current capacity current_life_minutes = round( ((before._energy / drain_per_second) / 60), 2) print("Battery Life with at current battery capacity: %.2f minutes" % (current_life_minutes)) return 0 def main(): parser = argparse.ArgumentParser( description="""Determine battery drain and battery life by running the specified action. Battery life is shown for: current capacity, capacity when battery is full, and capacity when battery is full and was brand new (design capacity)""") parser.add_argument('-i', '--idle', help="Run the test while system is" " idling", action='store_true') parser.add_argument('-s3', '--sleep', help="Run the test while system" " is suspended", action='store_true') parser.add_argument('-t', '--time', help="Specify the allotted time in seconds to run", type=int, required=True) parser.add_argument('-m', '--movie', help="Run the test while playing the file MOVIE") args = parser.parse_args() test_time = args.time battery_before = get_battery_state() if not validate_battery_info(battery_before): return 1 print(battery_before) if args.idle: time.sleep(test_time) elif args.movie: totem_settings = Gio.Settings.new("org.gnome.totem") totem_settings.set_boolean("repeat", True) a = subprocess.Popen(['totem', '--fullscreen', args.movie]) time.sleep(test_time) a.kill() totem_settings = Gio.Settings.new("org.gnome.totem") totem_settings.set_boolean("repeat", False) elif args.sleep: subprocess.call(['fwts', 's3', '--s3-sleep-delay=' + str(test_time)]) battery_after = get_battery_state() if not validate_battery_info(battery_after): return 1 print(battery_after) return(battery_life(battery_before, battery_after, test_time)) if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/disk_smart0000775000175000017500000002061412320541306023661 0ustar zygazyga00000000000000#!/usr/bin/env python3 ''' Script to automate disk SMART testing Copyright (C) 2010 Canonical Ltd. Authors Jeff Lane Brendan Donegan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . The purpose of this script is to simply interact with an onboard hard disk and check for SMART capability and then do a little bit of interaction to make sure we can at least do some limited interaction with the hard disk's SMART functions. In this case, we probe to see if SMART is available and enabled, then we run the short self test. Return 0 if it's all good, return 1 if it fails. NOTE: This may not work correctly on systems where the onboard storage is controlled by a hardware RAID controller, on external RAID systems, SAN, and USB/eSATA/eSAS attached storage devices. Changelog: v1.1: Put delay before first attempt to acces log, rather than after v1.0: added debugger class and code to allow for verbose debug output if needed v0.4: corrected some minor things added option parsing to allow for many disks, or disks other than "/dev/sda" V0.3: Removed the arbitrary wait time and implemented a polling method to shorten the test time. Added in Pass/Fail criteria for the final outcome. Added in documentation. V0.2: added minor debug routine V0.1: Fixed some minor bugs and added the SmartEnabled() function V0: First draft ''' import os import sys import time import logging from subprocess import Popen, PIPE from argparse import ArgumentParser class ListHandler(logging.StreamHandler): def emit(self, record): if isinstance(record.msg, (list, tuple)): for msg in record.msg: if type(msg) is bytes: msg = msg.decode() logger = logging.getLogger(record.name) new_record = logger.makeRecord(record.name, record.levelno, record.pathname, record.lineno, msg, record.args, record.exc_info, record.funcName) logging.StreamHandler.emit(self, new_record) else: logging.StreamHandler.emit(self, record) def is_smart_enabled(disk): # Check with smartctl to see if SMART is available and enabled on the disk command = 'smartctl -i %s' % disk diskinfo_bytes = (Popen(command, stdout=PIPE, shell=True) .communicate()[0]) diskinfo = diskinfo_bytes.decode().splitlines() logging.debug('SMART Info for disk %s', disk) logging.debug(diskinfo) return (len(diskinfo) > 2 and 'Enabled' in diskinfo[-2] and 'Available' in diskinfo[-3]) def run_smart_test(disk, type='short'): ctl_command = 'smartctl -t %s %s' % (type, disk) logging.debug('Beginning test with %s', ctl_command) smart_proc = Popen(ctl_command, stderr=PIPE, stdout=PIPE, universal_newlines=True, shell=True) ctl_output, ctl_error = smart_proc.communicate() logging.debug(ctl_error + ctl_output) return smart_proc.returncode def get_smart_entries(disk, type='selftest'): entries = [] command = 'smartctl -l %s %s' % (type, disk) stdout = Popen(command, stdout=PIPE, shell=True).stdout # Skip intro lines while True: line = stdout.readline().decode() if not line: raise Exception('Failed to parse SMART log entries') if line.startswith('SMART'): break # Get lengths from header line = stdout.readline().decode() if not line.startswith('Num'): return entries columns = ['number', 'description', 'status', 'remaining', 'lifetime', 'lba'] lengths = [line.index(i) for i in line.split()] lengths[columns.index('remaining')] += len('Remaining') - len('100%') lengths.append(len(line)) # Get remaining lines entries = [] for line_bytes in stdout.readlines(): line = line_bytes.decode() if line.startswith('#'): entry = {} for i, column in enumerate(columns): entry[column] = line[lengths[i]:lengths[i + 1]].strip() # Convert some columns to integers entry['number'] = int(entry['number'][1:]) entry['lifetime'] = int(entry['lifetime']) entries.append(entry) return entries def main(): description = 'Tests that SMART capabilities on disks that support SMART function.' parser = ArgumentParser(description=description) parser.add_argument('-b', '--block-dev', metavar='DISK', default='/dev/sda', help=('the DISK to run this test against ' '[default: %(default)s]')) parser.add_argument('-d', '--debug', action='store_true', default=False, help='prints some debug info') parser.add_argument('-s', '--sleep', type=int, default=5, help=('number of seconds to sleep between checks ' '[default: %(default)s].')) parser.add_argument('-t', '--timeout', type=int, help='number of seconds to timeout from sleeping.') args = parser.parse_args() # Set logging format = '%(levelname)-8s %(message)s' handler = ListHandler() handler.setFormatter(logging.Formatter(format)) logger = logging.getLogger() logger.addHandler(handler) if args.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) # Make sure we're root, because smartctl doesn't work otherwise. if not os.geteuid()==0: parser.error("You must be root to run this program") # If SMART is available and enabled, we proceed. Otherwise, we exit as the # test is pointless in this case. disk = args.block_dev if not is_smart_enabled(disk): logging.warning('SMART not available on %s' % disk) return 0 # Initiate a self test and start polling until the test is done previous_entries = get_smart_entries(disk) logging.info("Starting SMART self-test on %s" % disk) if run_smart_test(disk) != 0: logging.error("Error reported during smartctl test") return 1 if len(previous_entries) > 20: # Abort the previous instance # so that polling can identify the difference run_smart_test(disk) previous_entries = get_smart_entries(disk) # Priming read... this is here in case our test is finished or fails # immediate after it begins. logging.debug('Polling selftest.log for status') while True: # Poll every sleep seconds until test is complete$ time.sleep(args.sleep) current_entries = get_smart_entries(disk) logging.debug('%s %s %s %s' % (current_entries[0]['number'], current_entries[0]['description'], current_entries[0]['status'], current_entries[0]['remaining'])) if current_entries != previous_entries \ and current_entries[0]["status"] != 'Self-test routine in progress': break if args.timeout is not None: if args.timeout <= 0: logging.debug('Polling timed out') return 1 else: args.timeout -= args.sleep status = current_entries[0]['status'] if status != 'Completed without error': log = get_smart_entries(disk) logging.error("FAIL: SMART Self-Test appears to have failed for some reason. " "Run 'sudo smartctl -l selftest %s' to see the SMART log" % disk) logging.debug("Last self-test run status: %s" % status) return 1 else: logging.info("PASS: SMART Self-Test completed without error") return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/audio_settings0000775000175000017500000000021312320541306024533 0ustar zygazyga00000000000000#!/usr/bin/python3 import sys from checkbox_support.scripts.audio_settings import main if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/pm_test0000775000175000017500000007633412320541306023206 0ustar zygazyga00000000000000#!/usr/bin/env python3 import logging import logging.handlers import os import pwd import re import shutil import subprocess import sys from argparse import ArgumentParser, SUPPRESS from calendar import timegm from datetime import datetime, timedelta from gi.repository import Gtk, GObject from time import time, localtime def main(): """ Run power management operation as many times as needed """ args, extra_args = MyArgumentParser().parse() # Verify that script is run as root if os.getuid(): sys.stderr.write('This script needs superuser ' 'permissions to run correctly\n') sys.exit(1) #Obtain name of the invoking user. uid = os.getenv('SUDO_UID') or os.getenv('PKEXEC_UID') if not uid: sys.stderr.write('Unable to determine invoking user\n') sys.exit(1) username = pwd.getpwuid(int(uid)).pw_name LoggingConfiguration.set(args.log_level, args.log_filename, args.append) logging.debug('Invoking username: %s', username) logging.debug('Arguments: {0!r}'.format(args)) logging.debug('Extra Arguments: {0!r}'.format(extra_args)) try: operation = PowerManagementOperation(args, extra_args, user=username) operation.setup() operation.run() except (TestCancelled, TestFailed) as exception: operation.teardown() if isinstance(exception, TestFailed): logging.error(exception.args[0]) message = exception.MESSAGE.format(args.pm_operation.capitalize()) if args.silent: logging.info(message) else: title = '{0} test'.format(args.pm_operation.capitalize()) MessageDialog(title, message, Gtk.MessageType.ERROR).run() return exception.RETURN_CODE return 0 class PowerManagementOperation(object): SLEEP_TIME = 5 def __init__(self, args, extra_args, user=None): self.args = args self.extra_args = extra_args self.user = user def setup(self): """ Enable configuration file """ # Enable autologin and sudo on first cycle if self.args.total == self.args.repetitions: AutoLoginConfigurator(user=self.user).enable() SudoersConfigurator(user=self.user).enable() # Schedule this script to be automatically executed # on startup to continue testing autostart_file = AutoStartFile(self.args, user=self.user) autostart_file.write() def run(self): """ Run a power management iteration """ logging.info('{0} operations remaining: {1}' .format(self.args.pm_operation, self.args.repetitions)) self.check_last_cycle_duration() if self.args.repetitions > 0: self.run_pm_command() else: self.summary() def check_last_cycle_duration(self): """ Make sure that last cycle duration was reasonable, that is, not too short, not too long """ min_pm_time = timedelta(seconds=self.args.min_pm_time) max_pm_time = timedelta(seconds=self.args.max_pm_time) if self.args.pm_timestamp: pm_timestamp = datetime.fromtimestamp(self.args.pm_timestamp) now = datetime.now() pm_time = now - pm_timestamp if pm_time < min_pm_time: raise TestFailed('{0} time less than expected: {1} < {2}' .format(self.args.pm_operation.capitalize(), pm_time, min_pm_time)) if pm_time > max_pm_time: raise TestFailed('{0} time greater than expected: {1} > {2}' .format(self.args.pm_operation.capitalize(), pm_time, max_pm_time)) logging.info('{0} time: {1}' .format(self.args.pm_operation.capitalize(), pm_time)) def run_pm_command(self): """ Run power managment command and check result if needed """ # Display information to user # and make it possible to cancel the test CountdownDialog(self.args.pm_operation, self.args.pm_delay, self.args.hardware_delay, self.args.total - self.args.repetitions, self.args.total).run() # A small sleep time is added to reboot and poweroff # so that script has time to return a value # (useful when running it as an automated test) command_str = ('sleep {0}; {1}' .format(self.SLEEP_TIME, self.args.pm_operation)) if self.extra_args: command_str += ' {0}'.format(' '.join(self.extra_args)) if self.args.pm_operation != 'reboot': WakeUpAlarm.set(seconds=self.args.wakeup) logging.info('Executing new {0!r} operation...' .format(self.args.pm_operation)) logging.debug('Executing: {0!r}...'.format(command_str)) subprocess.Popen(command_str, shell=True) def summary(self): """ Gather hardware information for the last time, log execution time and exit """ # Just gather hardware information one more time and exit CountdownDialog(self.args.pm_operation, self.args.pm_delay, self.args.hardware_delay, self.args.total - self.args.repetitions, self.args.total).run() self.teardown() # Log some time information start = datetime.fromtimestamp(self.args.start) end = datetime.now() if self.args.pm_operation == 'reboot': sleep_time = timedelta(seconds=self.SLEEP_TIME) else: sleep_time = timedelta(seconds=self.args.wakeup) wait_time = timedelta(seconds=(self.args.pm_delay + (self.args.hardware_delay) * self.args.total)) average = (end - start - wait_time) / self.args.total - sleep_time time_message = ('Total elapsed time: {total}\n' 'Average recovery time: {average}' .format(total=end - start, average=average)) logging.info(time_message) message = ('{0} test complete' .format(self.args.pm_operation.capitalize())) if self.args.silent: logging.info(message) else: title = '{0} test'.format(self.args.pm_operation.capitalize()) MessageDialog(title, message).run() def teardown(self): """ Restore configuration """ # Don't execute this script again on next reboot autostart_file = AutoStartFile(self.args, user=self.user) autostart_file.remove() # Restore previous configuration SudoersConfigurator().disable() AutoLoginConfigurator().disable() class TestCancelled(Exception): RETURN_CODE = 1 MESSAGE = '{0} test cancelled by user' class TestFailed(Exception): RETURN_CODE = 2 MESSAGE = '{0} test failed' class WakeUpAlarm(object): ALARM_FILENAME = '/sys/class/rtc/rtc0/wakealarm' RTC_FILENAME = '/proc/driver/rtc' @classmethod def set(cls, minutes=0, seconds=0): """ Calculate wakeup time and write it to BIOS """ now = int(time()) timeout = minutes * 60 + seconds wakeup_time_utc = now + timeout wakeup_time_local = timegm(localtime()) + timeout subprocess.check_call('echo 0 > %s' % cls.ALARM_FILENAME, shell=True) subprocess.check_call('echo %d > %s' % (wakeup_time_utc, cls.ALARM_FILENAME), shell=True) with open(cls.ALARM_FILENAME) as alarm_file: wakeup_time_stored_str = alarm_file.read() if not re.match('\d+', wakeup_time_stored_str): subprocess.check_call('echo "+%d" > %s' % (timeout, cls.ALARM_FILENAME), shell=True) with open(cls.ALARM_FILENAME) as alarm_file2: wakeup_time_stored_str = alarm_file2.read() if not re.match('\d+', wakeup_time_stored_str): logging.error('Invalid wakeup time format: {0!r}' .format(wakeup_time_stored_str)) sys.exit(1) wakeup_time_stored = int(wakeup_time_stored_str) try: logging.debug('Wakeup timestamp: {0} ({1})' .format(wakeup_time_stored, datetime.fromtimestamp( wakeup_time_stored).strftime('%c'))) except ValueError as e: logging.error(e) sys.exit(1) if ((abs(wakeup_time_utc - wakeup_time_stored) > 1) and (abs(wakeup_time_local - wakeup_time_stored) > 1)): logging.error('Wakeup time not stored correctly') sys.exit(1) with open(cls.RTC_FILENAME) as rtc_file: separator_regex = re.compile('\s+:\s+') rtc_data = dict([separator_regex.split(line.rstrip()) for line in rtc_file]) logging.debug('RTC data:\n{0}' .format('\n'.join(['- {0}: {1}'.format(*pair) for pair in rtc_data.items()]))) # Verify wakeup time has been set properly # by looking into the alarm_IRQ and alrm_date field if rtc_data['alarm_IRQ'] != 'yes': logging.error('alarm_IRQ not set properly: {0}' .format(rtc_data['alarm_IRQ'])) sys.exit(1) if '*' in rtc_data['alrm_date']: logging.error('alrm_date not set properly: {0}' .format(rtc_data['alrm_date'])) sys.exit(1) class Command(object): """ Simple subprocess.Popen wrapper to run shell commands and log their output """ def __init__(self, command_str, verbose=True): self.command_str = command_str self.verbose = verbose self.process = None self.stdout = None self.stderr = None self.time = None def run(self): """ Execute shell command and return output and status """ logging.debug('Executing: {0!r}...'.format(self.command_str)) self.process = subprocess.Popen(self.command_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) start = datetime.now() result = self.process.communicate() end = datetime.now() self.time = end - start if self.verbose: stdout, stderr = result message = ['Output:\n' '- returncode:\n{0}'.format(self.process.returncode)] if stdout: if type(stdout) is bytes: stdout = stdout.decode('utf-8') message.append('- stdout:\n{0}'.format(stdout)) if stderr: if type(stderr) is bytes: stderr = stderr.decode('utf-8') message.append('- stderr:\n{0}'.format(stderr)) logging.debug('\n'.join(message)) self.stdout = stdout self.stderr = stderr return self class CountdownDialog(Gtk.Dialog): """ Dialog that shows the amount of progress in the reboot test and lets the user cancel it if needed """ def __init__(self, pm_operation, pm_delay, hardware_delay, iterations, iterations_count): self.pm_operation = pm_operation title = '{0} test'.format(pm_operation.capitalize()) buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,) super(CountdownDialog, self).__init__(title=title, buttons=buttons) self.set_default_response(Gtk.ResponseType.CANCEL) self.set_resizable(False) self.set_position(Gtk.WindowPosition.CENTER) progress_bar = Gtk.ProgressBar() progress_bar.set_fraction(iterations / float(iterations_count)) progress_bar.set_text('{0}/{1}' .format(iterations, iterations_count)) progress_bar.set_show_text(True) self.vbox.pack_start(progress_bar, True, True, 0) operation_event = {'template': ('Next {0} in {{time}} seconds...' .format(self.pm_operation)), 'timeout': pm_delay} hardware_info_event = \ {'template': 'Gathering hardware information in {time} seconds...', 'timeout': hardware_delay, 'callback': self.on_hardware_info_timeout_cb} if iterations == 0: # In first iteration, gather hardware information directly # and perform pm-operation self.on_hardware_info_timeout_cb() self.events = [operation_event] elif iterations < iterations_count: # In last iteration, wait before gathering hardware information # and perform pm-operation self.events = [operation_event, hardware_info_event] else: # In last iteration, wait before gathering hardware information # and finish the test self.events = [hardware_info_event] self.label = Gtk.Label() self.vbox.pack_start(self.label, True, True, 0) self.show_all() def run(self): """ Set label text and run dialog """ self.schedule_next_event() response = super(CountdownDialog, self).run() self.destroy() if response != Gtk.ResponseType.ACCEPT: raise TestCancelled() def schedule_next_event(self): """ Schedule next timed event """ if self.events: self.event = self.events.pop() self.timeout_counter = self.event.get('timeout', 0) self.label.set_text(self.event['template'] .format(time=self.timeout_counter)) GObject.timeout_add_seconds(1, self.on_timeout_cb) else: # Return Accept response # if there are no other events scheduled self.response(Gtk.ResponseType.ACCEPT) def on_timeout_cb(self): """ Set label properly and use callback method if needed """ if self.timeout_counter > 0: self.label.set_text(self.event['template'] .format(time=self.timeout_counter)) self.timeout_counter -= 1 return True # Call calback if defined callback = self.event.get('callback') if callback: callback() # Schedule next event if needed self.schedule_next_event() return False def on_hardware_info_timeout_cb(self): """ Gather hardware information and print it to logs """ logging.info('Gathering hardware information...') logging.debug('Networking:\n' '{network}\n' '{ethernet}\n' '{ifconfig}\n' '{iwconfig}' .format(network=(Command('lspci | grep Network') .run().stdout), ethernet=(Command('lspci | grep Ethernet') .run().stdout), ifconfig=(Command("ifconfig -a | grep -A1 '^\w'") .run().stdout), iwconfig=(Command("iwconfig | grep -A1 '^\w'") .run().stdout))) logging.debug('Bluetooth Device:\n' '{hciconfig}' .format(hciconfig=(Command("hciconfig -a " "| grep -A2 '^\w'") .run().stdout))) logging.debug('Video Card:\n' '{lspci}' .format(lspci=Command('lspci | grep VGA').run().stdout)) logging.debug('Touchpad and Keyboard:\n' '{xinput}' .format(xinput=Command( 'xinput list --name-only | sort').run().stdout)) logging.debug('Pulse Audio Sink:\n' '{pactl_sink}' .format(pactl_sink=(Command('pactl list | grep Sink') .run().stdout))) logging.debug('Pulse Audio Source:\n' '{pactl_source}' .format(pactl_source=(Command('pactl list | grep Source') .run().stdout))) # Check kernel logs using firmware test suite command = Command('fwts -r stdout klog oops').run() if command.process.returncode != 0: # Don't abort the test loop, # errors can be retrieved by pm_log_check logging.error('Problem found in logs by fwts') class MessageDialog(object): """ Simple wrapper aroung Gtk.MessageDialog """ def __init__(self, title, message, type=Gtk.MessageType.INFO): self.title = title self.message = message self.type = type def run(self): dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.OK, message_format=self.message, type=self.type) logging.info(self.message) dialog.set_title(self.title) dialog.run() dialog.destroy() class AutoLoginConfigurator(object): """ Enable/disable autologin configuration to make sure that reboot test will work properly """ CONFIG_FILENAME = '/etc/lightdm/lightdm.conf' TEMPLATE = """ [SeatDefaults] greeter-session=unity-greeter user-session=ubuntu autologin-user={username} autologin-user-timeout=0 """ def __init__(self, user=None): self.user = user def enable(self): """ Make sure user will autologin in next reboot """ logging.debug('Enabling autologin for this user...') if os.path.exists(self.CONFIG_FILENAME): for backup_filename in self.generate_backup_filename(): if not os.path.exists(backup_filename): shutil.copyfile(self.CONFIG_FILENAME, backup_filename) shutil.copystat(self.CONFIG_FILENAME, backup_filename) break with open(self.CONFIG_FILENAME, 'w') as f: f.write(self.TEMPLATE.format(username=self.user)) def disable(self): """ Remove latest configuration file and use the same configuration that was in place before running the test """ logging.debug('Restoring autologin configuration...') backup_filename = None for filename in self.generate_backup_filename(): if not os.path.exists(filename): break backup_filename = filename if backup_filename: shutil.copy(backup_filename, self.CONFIG_FILENAME) os.remove(backup_filename) else: os.remove(self.CONFIG_FILENAME) def generate_backup_filename(self): backup_filename = self.CONFIG_FILENAME + '.bak' yield backup_filename index = 0 while True: index += 1 backup_filename = (self.CONFIG_FILENAME + '.bak.{0}'.format(index)) yield backup_filename class SudoersConfigurator(object): """ Enable/disable reboot test to be executed as root to make sure that reboot test works properly """ MARK = '# Automatically added by pm.py' SUDOERS = '/etc/sudoers' def __init__(self, user=None): self.user = user def enable(self): """ Make sure that user will be allowed to execute reboot test as root """ logging.debug('Enabling user to execute test as root...') command = ("sed -i -e '$a{mark}\\n" "{user} ALL=NOPASSWD: /usr/bin/python' " "{filename}".format(mark=self.MARK, user=self.user, script=os.path.realpath(__file__), filename=self.SUDOERS)) Command(command, verbose=False).run() def disable(self): """ Revert sudoers configuration changes """ logging.debug('Restoring sudoers configuration...') command = (("sed -i -e '/{mark}/,+1d' " "{filename}") .format(mark=self.MARK, filename=self.SUDOERS)) Command(command, verbose=False).run() class AutoStartFile(object): """ Generate autostart file contents and write it to proper location """ TEMPLATE = """ [Desktop Entry] Name={pm_operation} test Comment=Verify {pm_operation} works properly Exec=sudo /usr/bin/python {script} -r {repetitions} -w {wakeup} --hardware-delay {hardware_delay} --pm-delay {pm_delay} --min-pm-time {min_pm_time} --max-pm-time {max_pm_time} --append --total {total} --start {start} --pm-timestamp {pm_timestamp} {silent} --log-level={log_level} --log-dir={log_dir} {pm_operation} Type=Application X-GNOME-Autostart-enabled=true Hidden=false """ def __init__(self, args, user=None): self.args = args self.user = user # Generate desktop filename # based on environment variables username = self.user default_config_directory = os.path.expanduser('~{0}/.config' .format(username)) config_directory = os.getenv('XDG_CONFIG_HOME', default_config_directory) autostart_directory = os.path.join(config_directory, 'autostart') if not os.path.exists(autostart_directory): os.makedirs(autostart_directory) basename = '{0}.desktop'.format(os.path.basename(__file__)) self.desktop_filename = os.path.join(autostart_directory, basename) def write(self): """ Write autostart file to execute the script on startup """ logging.debug('Writing desktop file ({0!r})...' .format(self.desktop_filename)) contents = (self.TEMPLATE .format(script=os.path.realpath(__file__), repetitions=self.args.repetitions - 1, wakeup=self.args.wakeup, hardware_delay=self.args.hardware_delay, pm_delay=self.args.pm_delay, min_pm_time=self.args.min_pm_time, max_pm_time=self.args.max_pm_time, total=self.args.total, start=self.args.start, pm_timestamp=int(time()), silent='--silent' if self.args.silent else '', log_level=self.args.log_level_str, log_dir=self.args.log_dir, pm_operation=self.args.pm_operation)) logging.debug(contents) with open(self.desktop_filename, 'w') as f: f.write(contents) def remove(self): """ Remove autostart file to avoid executing the script on startup """ if os.path.exists(self.desktop_filename): logging.debug('Removing desktop file ({0!r})...' .format(self.desktop_filename)) os.remove(self.desktop_filename) class LoggingConfiguration(object): @classmethod def set(cls, log_level, log_filename, append): """ Configure a rotating file logger """ logger = logging.getLogger() logger.setLevel(logging.DEBUG) # Log to sys.stderr using log level passed through command line if log_level != logging.NOTSET: log_handler = logging.StreamHandler() formatter = logging.Formatter('%(levelname)-8s %(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(log_level) logger.addHandler(log_handler) # Log to rotating file using DEBUG log level log_handler = logging.handlers.RotatingFileHandler(log_filename, mode='a+', backupCount=3) formatter = logging.Formatter('%(asctime)s %(levelname)-8s ' '%(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(logging.DEBUG) logger.addHandler(log_handler) if not append: # Create a new log file on every new # (i.e. not scheduled) invocation log_handler.doRollover() class MyArgumentParser(object): """ Command-line argument parser """ def __init__(self): """ Create parser object """ pm_operations = ('poweroff', 'reboot') description = 'Run power management operation as many times as needed' epilog = ('Unknown arguments will be passed ' 'to the underlying command: poweroff or reboot.') parser = ArgumentParser(description=description, epilog=epilog) parser.add_argument('-r', '--repetitions', type=int, default=1, help=('Number of times that the power management ' 'operation has to be repeated ' '(%(default)s by default)')) parser.add_argument('-w', '--wakeup', type=int, default=60, help=('Timeout in seconds for the wakeup alarm ' '(%(default)s by default). ' "Note: wakeup alarm won't be scheduled " 'for reboot.')) parser.add_argument('--min-pm-time', dest='min_pm_time', type=int, default=0, help=('Minimum time in seconds that ' 'it should take the power management ' 'operation each cycle (0 for reboot and ' 'wakeup time minus two seconds ' 'for the other power management operations ' 'by default)')) parser.add_argument('--max-pm-time', dest='max_pm_time', type=int, default=300, help=('Maximum time in seconds ' 'that it should take ' 'the power management operation each cycle ' '(%(default)s by default)')) parser.add_argument('--pm-delay', dest='pm_delay', type=int, default=5, help=('Delay in seconds ' 'after hardware information ' 'has been gathered and before executing ' 'the power management operation ' '(%(default)s by default)')) parser.add_argument('--hardware-delay', dest='hardware_delay', type=int, default=30, help=('Delay in seconds before gathering hardware ' 'information (%(default)s by default)')) parser.add_argument('--silent', action='store_true', help=("Don't display any dialog " 'when test is complete ' 'to let the script be used ' 'in automated tests')) log_levels = ['notset', 'debug', 'info', 'warning', 'error', 'critical'] parser.add_argument('--log-level', dest='log_level_str', default='info', choices=log_levels, help=('Log level. ' 'One of {0} or {1} (%(default)s by default)' .format(', '.join(log_levels[:-1]), log_levels[-1]))) parser.add_argument('--log-dir', dest='log_dir', default='/var/log', help=('Path to the directory to store log files')) parser.add_argument('pm_operation', choices=pm_operations, help=('Power management operation to be performed ' '(one of {0} or {1!r})' .format(', '.join(map(repr, pm_operations[:-1])), pm_operations[-1]))) # Test timestamps parser.add_argument('--start', type=int, default=0, help=SUPPRESS) parser.add_argument('--pm-timestamp', dest='pm_timestamp', type=int, default=0, help=SUPPRESS) # Append to log on subsequent startups parser.add_argument('--append', action='store_true', default=False, help=SUPPRESS) # Total number of iterations initially passed through the command line parser.add_argument('--total', type=int, default=0, help=SUPPRESS) self.parser = parser def parse(self): """ Parse command-line arguments """ args, extra_args = self.parser.parse_known_args() args.log_level = getattr(logging, args.log_level_str.upper()) # Total number of repetitions # is the number of repetitions passed through the command line # the first time the script is executed if not args.total: args.total = args.repetitions # Test start time automatically set on first iteration if not args.start: args.start = int(time()) # Wakeup time set to 0 for 'reboot' # since wakeup alarm won't be scheduled if args.pm_operation == 'reboot': args.wakeup = 0 args.min_pm_time = 0 # Minimum time for each power management operation # is set to the wakeup time if not args.min_pm_time: min_pm_time = args.wakeup - 2 if min_pm_time < 0: min_pm_time = 0 args.min_pm_time = min_pm_time # Log filename shows clearly the type of test (pm_operation) # and the times it was repeated (repetitions) args.log_filename = os.path.join(args.log_dir, ('{0}.{1}.{2}.log' .format(os.path.basename(__file__), args.pm_operation, args.total))) return args, extra_args if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/pts_run0000775000175000017500000000146612320541306023217 0ustar zygazyga00000000000000#!/bin/bash # Exit on any error set -o errexit # Accept Terms & Conditions, disable anonymous reporting echo -e "Y\nn\nn" | phoronix-test-suite > /dev/null # Disable batch result saving and all test options selection echo -e "n\nn" | phoronix-test-suite batch-setup > /dev/null # Run each test only one time export FORCE_TIMES_TO_RUN=1 # Run only the following resolution export OVERRIDE_VIDEO_MODES=800x600 set +o errexit rv=0 output=$(phoronix-test-suite batch-benchmark $@ 2>&1) #The output does NOT report success. It may contain, if it fails: # The test did not produce a result # The test failed to run properly # Failed to Fetch if (echo "$output" | grep -q -i "Failed to fetch" ); then rv=1 fi if ( echo "$output" | grep -q -i 'This test failed to run properly'); then rv=1 fi echo "$output" exit $rv 2013.com.canonical.certification.checkbox-0.4/bin/alsa_info0000775000175000017500000006621112320541306023457 0ustar zygazyga00000000000000#!/bin/bash SCRIPT_VERSION=0.4.61 CHANGELOG="http://www.alsa-project.org/alsa-info.sh.changelog" ################################################################################# #Copyright (C) 2007 Free Software Foundation. #This program is free software; you can redistribute it and/or modify #it under the terms of the GNU General Public License as published by #the Free Software Foundation; either version 2 of the License, or #(at your option) any later version. #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ################################################################################## #The script was written for 2 main reasons: # 1. Remove the need for the devs/helpers to ask several questions before we can easily help the user. # 2. Allow newer/inexperienced ALSA users to give us all the info we need to help them. #Set the locale (this may or may not be a good idea.. let me know) export LC_ALL=C #Change the PATH variable, so we can run lspci (needed for some distros) PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin BGTITLE="ALSA-Info v $SCRIPT_VERSION" PASTEBINKEY="C9cRIO8m/9y8Cs0nVs0FraRx7U0pHsuc" #Define some simple functions pbcheck(){ [[ $UPLOAD = "no" ]] && return if [[ -z $PASTEBIN ]]; then [[ $(ping -c1 www.alsa-project.org) ]] || KEEP_FILES="yes" UPLOAD="no" PBERROR="yes" else [[ $(ping -c1 www.pastebin.ca) ]] || KEEP_FILES="yes" UPLOAD="no" PBERROR="yes" fi } update() { SHFILE=`mktemp -t alsa-info.XXXXXXXXXX` || exit 1 wget -O $SHFILE "http://www.alsa-project.org/alsa-info.sh" >/dev/null 2>&1 REMOTE_VERSION=`grep SCRIPT_VERSION $SHFILE |head -n1 |sed 's/.*=//'` if [ "$REMOTE_VERSION" != "$SCRIPT_VERSION" ]; then if [[ -n $DIALOG ]] then OVERWRITE= if [ -w $0 ]; then dialog --yesno "Newer version of ALSA-Info has been found\n\nDo you wish to install it?\nNOTICE: The original file $0 will be overwritten!" 0 0 DIALOG_EXIT_CODE=$? if [[ $DIALOG_EXIT_CODE = 0 ]]; then OVERWRITE=yes fi fi if [ -z "$OVERWRITE" ]; then dialog --yesno "Newer version of ALSA-Info has been found\n\nDo you wish to download it?" 0 0 DIALOG_EXIT_CODE=$? fi if [[ $DIALOG_EXIT_CODE = 0 ]] then echo "Newer version detected: $REMOTE_VERSION" echo "To view the ChangeLog, please visit $CHANGELOG" if [ "$OVERWRITE" = "yes" ]; then cp $SHFILE $0 echo "ALSA-Info script has been updated to v $REMOTE_VERSION" echo "Please re-run the script" rm $SHFILE 2>/dev/null else echo "ALSA-Info script has been downloaded as $SHFILE." echo "Please re-run the script from new location." fi exit else rm $SHFILE 2>/dev/null fi else echo "Newer version detected: $REMOTE_VERSION" echo "To view the ChangeLog, please visit $CHANGELOG" if [ -w $0 ]; then echo "The original file $0 will be overwritten!" echo -n "If you do not like to proceed, press Ctrl-C now.." ; read inp cp $SHFILE $0 echo "ALSA-Info script has been updated. Please re-run it." rm $SHFILE 2>/dev/null else echo "ALSA-Info script has been downloaded $SHFILE." echo "Please, re-run it from new location." fi exit fi else rm $SHFILE 2>/dev/null fi } cleanup() { if [ -n "$TEMPDIR" -a "$KEEP_FILES" != "yes" ]; then rm -rf "$TEMPDIR" 2>/dev/null fi test -n "$KEEP_OUTPUT" || rm -f "$NFILE" } withaplay() { echo "!!Aplay/Arecord output" >> $FILE echo "!!--------------------" >> $FILE echo "" >> $FILE echo "APLAY" >> $FILE echo "" >> $FILE aplay -l >> $FILE 2>&1 echo "" >> $FILE echo "ARECORD" >> $FILE echo "" >> $FILE arecord -l >> $FILE 2>&1 echo "" >> $FILE } withlsmod() { echo "!!All Loaded Modules" >> $FILE echo "!!------------------" >> $FILE echo "" >> $FILE lsmod |awk {'print $1'} >> $FILE echo "" >> $FILE echo "" >> $FILE } withamixer() { echo "!!Amixer output" >> $FILE echo "!!-------------" >> $FILE echo "" >> $FILE for i in `grep "]: " /proc/asound/cards | awk -F ' ' '{ print $1} '` ; do CARD_NAME=`grep "^ *$i " $TEMPDIR/alsacards.tmp|awk {'print $2'}` echo "!!-------Mixer controls for card $i $CARD_NAME]" >> $FILE echo "" >>$FILE amixer -c$i info>> $FILE 2>&1 amixer -c$i>> $FILE 2>&1 echo "" >> $FILE done echo "" >> $FILE } withalsactl() { echo "!!Alsactl output" >> $FILE echo "!!--------------" >> $FILE echo "" >> $FILE exe="" if [ -x /usr/sbin/alsactl ]; then exe="/usr/sbin/alsactl" fi if [ -x /usr/local/sbin/alsactl ]; then exe="/usr/local/sbin/alsactl" fi if [ -z "$exe" ]; then exe=`whereis alsactl | cut -d ' ' -f 2` fi $exe -f $TEMPDIR/alsactl.tmp store echo "--startcollapse--" >> $FILE cat $TEMPDIR/alsactl.tmp >> $FILE echo "--endcollapse--" >> $FILE echo "" >> $FILE echo "" >> $FILE } withdevices() { echo "!!ALSA Device nodes" >> $FILE echo "!!-----------------" >> $FILE echo "" >> $FILE ls -la /dev/snd/* >> $FILE echo "" >> $FILE echo "" >> $FILE } withconfigs() { if [[ -e $HOME/.asoundrc ]] || [[ -e /etc/asound.conf ]] || [[ -e $HOME/.asoundrc.asoundconf ]] then echo "!!ALSA configuration files" >> $FILE echo "!!------------------------" >> $FILE echo "" >> $FILE #Check for ~/.asoundrc if [[ -e $HOME/.asoundrc ]] then echo "!!User specific config file (~/.asoundrc)" >> $FILE echo "" >> $FILE cat $HOME/.asoundrc >> $FILE echo "" >> $FILE echo "" >> $FILE fi #Check for .asoundrc.asoundconf (seems to be Ubuntu specific) if [[ -e $HOME/.asoundrc.asoundconf ]] then echo "!!asoundconf-generated config file" >> $FILE echo "" >> $FILE cat $HOME/.asoundrc.asoundconf >> $FILE echo "" >> $FILE echo "" >> $FILE fi #Check for /etc/asound.conf if [[ -e /etc/asound.conf ]] then echo "!!System wide config file (/etc/asound.conf)" >> $FILE echo "" >> $FILE cat /etc/asound.conf >> $FILE echo "" >> $FILE echo "" >> $FILE fi fi } withsysfs() { local i f local printed="" for i in /sys/class/sound/*; do case "$i" in */hwC?D?) if [ -f $i/init_pin_configs ]; then if [ -z "$printed" ]; then echo "!!Sysfs Files" >> $FILE echo "!!-----------" >> $FILE echo "" >> $FILE fi for f in init_pin_configs driver_pin_configs user_pin_configs init_verbs; do echo "$i/$f:" >> $FILE cat $i/$f >> $FILE echo >> $FILE done printed=yes fi ;; esac done if [ -n "$printed" ]; then echo "" >> $FILE fi } withdmesg() { echo "!!ALSA/HDA dmesg" >> $FILE echo "!!--------------" >> $FILE echo "" >> $FILE dmesg | grep -C1 -E 'ALSA|HDA|HDMI|sound|hda.codec|hda.intel' >> $FILE echo "" >> $FILE echo "" >> $FILE } withall() { withdevices withconfigs withaplay withamixer withalsactl withlsmod withsysfs withdmesg } get_alsa_library_version() { ALSA_LIB_VERSION=`grep VERSION_STR /usr/include/alsa/version.h 2>/dev/null|awk {'print $3'}|sed 's/"//g'` if [ -z "$ALSA_LIB_VERSION" ]; then if [ -f /etc/lsb-release ]; then . /etc/lsb-release case "$DISTRIB_ID" in Ubuntu) if which dpkg > /dev/null ; then ALSA_LIB_VERSION=`dpkg -l libasound2 | tail -1 | awk '{print $3}' | cut -f 1 -d -` fi if [ "$ALSA_LIB_VERSION" = "" ]; then ALSA_LIB_VERSION="" fi return ;; *) return ;; esac elif [ -f /etc/debian_version ]; then if which dpkg > /dev/null ; then ALSA_LIB_VERSION=`dpkg -l libasound2 | tail -1 | awk '{print $3}' | cut -f 1 -d -` fi if [ "$ALSA_LIB_VERSION" = "" ]; then ALSA_LIB_VERSION="" fi return fi fi } #Run checks to make sure the programs we need are installed. LSPCI=$(which lspci 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null); TPUT=$(which tput 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null); DIALOG=$(which dialog 2>/dev/null | sed 's|^[^/]*||' 2>/dev/null); #Check to see if sysfs is enabled in the kernel. We'll need this later on SYSFS=$(mount |grep sysfs|awk {'print $3'}); #Check modprobe config files for sound related options SNDOPTIONS=$(modprobe -c|sed -n 's/^options \(snd[-_][^ ]*\)/\1:/p') KEEP_OUTPUT= NFILE="" PASTEBIN="" WWWSERVICE="www.alsa-project.org" WELCOME="yes" PROCEED="yes" UPLOAD="ask" REPEAT="" while [ -z "$REPEAT" ]; do REPEAT="no" case "$1" in --update|--help|--about) WELCOME="no" PROCEED="no" ;; --upload) UPLOAD="yes" WELCOME="no" ;; --no-upload) UPLOAD="no" WELCOME="no" ;; --pastebin) PASTEBIN="yes" WWWSERVICE="pastebin" ;; --no-dialog) DIALOG="" REPEAT="" shift ;; --stdout) DIALOG="" UPLOAD="no" WELCOME="no" TOSTDOUT="yes" ;; esac done #Script header output. if [ "$WELCOME" = "yes" ]; then greeting_message="\ This script visits the following commands/files to collect diagnostic information about your ALSA installation and sound related hardware. dmesg lspci lsmod aplay amixer alsactl /proc/asound/ /sys/class/sound/ ~/.asoundrc (etc.) See '$0 --help' for command line options. " if [[ -n "$DIALOG" ]]; then dialog --backtitle "$BGTITLE" \ --title "ALSA-Info script v $SCRIPT_VERSION" \ --msgbox "$greeting_message" 20 80 else echo "ALSA Information Script v $SCRIPT_VERSION" echo "--------------------------------" echo "$greeting_message" fi # dialog fi # WELCOME #Set the output file TEMPDIR=`mktemp -t -d alsa-info.XXXXXXXXXX` || exit 1 FILE="$TEMPDIR/alsa-info.txt" if [ -z "$NFILE" ]; then NFILE=`mktemp -t alsa-info.txt.XXXXXXXXXX` || exit 1 fi trap cleanup 0 if [ "$PROCEED" = "yes" ]; then if [[ -z "$LSPCI" ]] then echo "This script requires lspci. Please install it, and re-run this script." exit 0 fi #Fetch the info and store in temp files/variables DISTRO=`grep -ihs "buntu\|SUSE\|Fedora\|PCLinuxOS\|MEPIS\|Mandriva\|Debian\|Damn\|Sabayon\|Slackware\|KNOPPIX\|Gentoo\|Zenwalk\|Mint\|Kubuntu\|FreeBSD\|Puppy\|Freespire\|Vector\|Dreamlinux\|CentOS\|Arch\|Xandros\|Elive\|SLAX\|Red\|BSD\|KANOTIX\|Nexenta\|Foresight\|GeeXboX\|Frugalware\|64\|SystemRescue\|Novell\|Solaris\|BackTrack\|KateOS\|Pardus" /etc/{issue,*release,*version}` KERNEL_VERSION=`uname -r` KERNEL_PROCESSOR=`uname -p` KERNEL_MACHINE=`uname -m` KERNEL_OS=`uname -o` [[ `uname -v |grep SMP` ]] && KERNEL_SMP="Yes" || KERNEL_SMP="No" ALSA_DRIVER_VERSION=`cat /proc/asound/version |head -n1|awk {'print $7'} |sed 's/\.$//'` get_alsa_library_version ALSA_UTILS_VERSION=`amixer -v |awk {'print $3'}` VENDOR_ID=`lspci -vn |grep 040[1-3] | awk -F':' '{print $3}'|awk {'print substr($0, 2);}' >$TEMPDIR/vendor_id.tmp` DEVICE_ID=`lspci -vn |grep 040[1-3] | awk -F':' '{print $4}'|awk {'print $1'} >$TEMPDIR/device_id.tmp` LAST_CARD=$((`grep "]: " /proc/asound/cards | wc -l` - 1 )) ESDINST=$(which esd 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) PAINST=$(which pulseaudio 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) ARTSINST=$(which artsd 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) JACKINST=$(which jackd 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) ROARINST=$(which roard 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) DMIDECODE=$(which dmidecode 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null) #Check for DMI data if [ -d /sys/class/dmi/id ]; then # No root privileges are required when using sysfs method DMI_SYSTEM_MANUFACTURER=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null) DMI_SYSTEM_PRODUCT_NAME=$(cat /sys/class/dmi/id/product_name 2>/dev/null) DMI_SYSTEM_PRODUCT_VERSION=$(cat /sys/class/dmi/id/product_version 2>/dev/null) DMI_SYSTEM_FIRMWARE_VERSION=$(cat /sys/class/dmi/id/bios_version 2>/dev/null) elif [ -x $DMIDECODE ]; then DMI_SYSTEM_MANUFACTURER=$($DMIDECODE -s system-manufacturer 2>/dev/null) DMI_SYSTEM_PRODUCT_NAME=$($DMIDECODE -s system-product-name 2>/dev/null) DMI_SYSTEM_PRODUCT_VERSION=$($DMIDECODE -s system-version 2>/dev/null) DMI_SYSTEM_FIRMWARE_VERSION=$($DMIDECODE -s bios-version 2>/dev/null) fi cat /proc/asound/modules 2>/dev/null|awk {'print $2'}>$TEMPDIR/alsamodules.tmp cat /proc/asound/cards >$TEMPDIR/alsacards.tmp lspci |grep -i "multi\|audio">$TEMPDIR/lspci.tmp #Check for HDA-Intel cards codec#* cat /proc/asound/card*/codec\#* > $TEMPDIR/alsa-hda-intel.tmp 2> /dev/null #Check for AC97 cards codec cat /proc/asound/card*/codec97\#0/ac97\#0-0 > $TEMPDIR/alsa-ac97.tmp 2> /dev/null cat /proc/asound/card*/codec97\#0/ac97\#0-0+regs > $TEMPDIR/alsa-ac97-regs.tmp 2> /dev/null #Check for USB mixer setup cat /proc/asound/card*/usbmixer > $TEMPDIR/alsa-usbmixer.tmp 2> /dev/null #Fetch the info, and put it in $FILE in a nice readable format. if [[ -z $PASTEBIN ]]; then echo "upload=true&script=true&cardinfo=" > $FILE else echo "name=$USER&type=33&description=/tmp/alsa-info.txt&expiry=&s=Submit+Post&content=" > $FILE fi echo "!!################################" >> $FILE echo "!!ALSA Information Script v $SCRIPT_VERSION" >> $FILE echo "!!################################" >> $FILE echo "" >> $FILE echo "!!Script ran on: `LANG=C TZ=UTC date`" >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!Linux Distribution" >> $FILE echo "!!------------------" >> $FILE echo "" >> $FILE echo $DISTRO >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!DMI Information" >> $FILE echo "!!---------------" >> $FILE echo "" >> $FILE echo "Manufacturer: $DMI_SYSTEM_MANUFACTURER" >> $FILE echo "Product Name: $DMI_SYSTEM_PRODUCT_NAME" >> $FILE echo "Product Version: $DMI_SYSTEM_PRODUCT_VERSION" >> $FILE echo "Firmware Version: $DMI_SYSTEM_FIRMWARE_VERSION" >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!Kernel Information" >> $FILE echo "!!------------------" >> $FILE echo "" >> $FILE echo "Kernel release: $KERNEL_VERSION" >> $FILE echo "Operating System: $KERNEL_OS" >> $FILE echo "Architecture: $KERNEL_MACHINE" >> $FILE echo "Processor: $KERNEL_PROCESSOR" >> $FILE echo "SMP Enabled: $KERNEL_SMP" >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!ALSA Version" >> $FILE echo "!!------------" >> $FILE echo "" >> $FILE echo "Driver version: $ALSA_DRIVER_VERSION" >> $FILE echo "Library version: $ALSA_LIB_VERSION" >> $FILE echo "Utilities version: $ALSA_UTILS_VERSION" >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!Loaded ALSA modules" >> $FILE echo "!!-------------------" >> $FILE echo "" >> $FILE cat $TEMPDIR/alsamodules.tmp >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!Sound Servers on this system" >> $FILE echo "!!----------------------------" >> $FILE echo "" >> $FILE if [[ -n $PAINST ]];then [[ `pgrep '^(.*/)?pulseaudio$'` ]] && PARUNNING="Yes" || PARUNNING="No" echo "Pulseaudio:" >> $FILE echo " Installed - Yes ($PAINST)" >> $FILE echo " Running - $PARUNNING" >> $FILE echo "" >> $FILE fi if [[ -n $ESDINST ]];then [[ `pgrep '^(.*/)?esd$'` ]] && ESDRUNNING="Yes" || ESDRUNNING="No" echo "ESound Daemon:" >> $FILE echo " Installed - Yes ($ESDINST)" >> $FILE echo " Running - $ESDRUNNING" >> $FILE echo "" >> $FILE fi if [[ -n $ARTSINST ]];then [[ `pgrep '^(.*/)?artsd$'` ]] && ARTSRUNNING="Yes" || ARTSRUNNING="No" echo "aRts:" >> $FILE echo " Installed - Yes ($ARTSINST)" >> $FILE echo " Running - $ARTSRUNNING" >> $FILE echo "" >> $FILE fi if [[ -n $JACKINST ]];then [[ `pgrep '^(.*/)?jackd$'` ]] && JACKRUNNING="Yes" || JACKRUNNING="No" echo "Jack:" >> $FILE echo " Installed - Yes ($JACKINST)" >> $FILE echo " Running - $JACKRUNNING" >> $FILE echo "" >> $FILE fi if [[ -n $ROARINST ]];then [[ `pgrep '^(.*/)?roard$'` ]] && ROARRUNNING="Yes" || ROARRUNNING="No" echo "RoarAudio:" >> $FILE echo " Installed - Yes ($ROARINST)" >> $FILE echo " Running - $ROARRUNNING" >> $FILE echo "" >> $FILE fi if [[ -z "$PAINST" && -z "$ESDINST" && -z "$ARTSINST" && -z "$JACKINST" && -z "$ROARINST" ]];then echo "No sound servers found." >> $FILE echo "" >> $FILE fi echo "" >> $FILE echo "!!Soundcards recognised by ALSA" >> $FILE echo "!!-----------------------------" >> $FILE echo "" >> $FILE cat $TEMPDIR/alsacards.tmp >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!PCI Soundcards installed in the system" >> $FILE echo "!!--------------------------------------" >> $FILE echo "" >> $FILE cat $TEMPDIR/lspci.tmp >> $FILE echo "" >> $FILE echo "" >> $FILE echo "!!Advanced information - PCI Vendor/Device/Subsystem ID's" >> $FILE echo "!!-------------------------------------------------------" >> $FILE echo "" >> $FILE lspci -vvn |grep -A1 040[1-3] >> $FILE echo "" >> $FILE echo "" >> $FILE if [ "$SNDOPTIONS" ] then echo "!!Modprobe options (Sound related)" >> $FILE echo "!!--------------------------------" >> $FILE echo "" >> $FILE modprobe -c|sed -n 's/^options \(snd[-_][^ ]*\)/\1:/p' >> $FILE echo "" >> $FILE echo "" >> $FILE fi if [ -d "$SYSFS" ] then echo "!!Loaded sound module options" >> $FILE echo "!!---------------------------" >> $FILE echo "" >> $FILE for mod in `cat /proc/asound/modules|awk {'print $2'}`;do echo "!!Module: $mod" >> $FILE for params in `echo $SYSFS/module/$mod/parameters/*`; do echo -ne "\t"; echo "$params : `cat $params`" | sed 's:.*/::'; done >> $FILE echo "" >> $FILE done echo "" >> $FILE fi if [ -s "$TEMPDIR/alsa-hda-intel.tmp" ] then echo "!!HDA-Intel Codec information" >> $FILE echo "!!---------------------------" >> $FILE echo "--startcollapse--" >> $FILE echo "" >> $FILE cat $TEMPDIR/alsa-hda-intel.tmp >> $FILE echo "--endcollapse--" >> $FILE echo "" >> $FILE echo "" >> $FILE fi if [ -s "$TEMPDIR/alsa-ac97.tmp" ] then echo "!!AC97 Codec information" >> $FILE echo "!!----------------------" >> $FILE echo "--startcollapse--" >> $FILE echo "" >> $FILE cat $TEMPDIR/alsa-ac97.tmp >> $FILE echo "" >> $FILE cat $TEMPDIR/alsa-ac97-regs.tmp >> $FILE echo "--endcollapse--" >> $FILE echo "" >> $FILE echo "" >> $FILE fi if [ -s "$TEMPDIR/alsa-usbmixer.tmp" ] then echo "!!USB Mixer information" >> $FILE echo "!!---------------------" >> $FILE echo "--startcollapse--" >> $FILE echo "" >> $FILE cat $TEMPDIR/alsa-usbmixer.tmp >> $FILE echo "--endcollapse--" >> $FILE echo "" >> $FILE echo "" >> $FILE fi #If no command line options are specified, then run as though --with-all was specified if [[ -z "$1" ]] then update withall pbcheck fi fi # proceed #loop through command line arguments, until none are left. if [[ -n "$1" ]] then until [ -z "$1" ] do case "$1" in --pastebin) update withall pbcheck ;; --update) update exit ;; --upload) UPLOAD="yes" withall ;; --no-upload) UPLOAD="no" withall ;; --output) shift NFILE="$1" KEEP_OUTPUT="yes" ;; --debug) echo "Debugging enabled. $FILE and $TEMPDIR will not be deleted" KEEP_FILES="yes" echo "" withall ;; --with-all) withall ;; --with-aplay) withaplay ;; --with-amixer) withamixer ;; --with-alsactl) withalsactl ;; --with-devices) withdevices ;; --with-dmesg) withdmesg ;; --with-configs) if [[ -e $HOME/.asoundrc ]] || [[ -e /etc/asound.conf ]] then echo "!!ALSA configuration files" >> $FILE echo "!!------------------------" >> $FILE echo "" >> $FILE #Check for ~/.asoundrc if [[ -e $HOME/.asoundrc ]] then echo "!!User specific config file ($HOME/.asoundrc)" >> $FILE echo "" >> $FILE cat $HOME/.asoundrc >> $FILE echo "" >> $FILE echo "" >> $FILE fi #Check for /etc/asound.conf if [[ -e /etc/asound.conf ]] then echo "!!System wide config file (/etc/asound.conf)" >> $FILE echo "" >> $FILE cat /etc/asound.conf >> $FILE echo "" >> $FILE echo "" >> $FILE fi fi ;; --stdout) UPLOAD="no" withall cat $FILE rm $FILE ;; --about) echo "Written/Tested by the following users of #alsa on irc.freenode.net:" echo "" echo " wishie - Script author and developer / Testing" echo " crimsun - Various script ideas / Testing" echo " gnubien - Various script ideas / Testing" echo " GrueMaster - HDA Intel specific items / Testing" echo " olegfink - Script update function" echo " TheMuso - display to stdout functionality" exit 0 ;; *) echo "alsa-info.sh version $SCRIPT_VERSION" echo "" echo "Available options:" echo " --with-aplay (includes the output of aplay -l)" echo " --with-amixer (includes the output of amixer)" echo " --with-alsactl (includes the output of alsactl)" echo " --with-configs (includes the output of ~/.asoundrc and" echo " /etc/asound.conf if they exist)" echo " --with-devices (shows the device nodes in /dev/snd/)" echo " --with-dmesg (shows the ALSA/HDA kernel messages)" echo "" echo " --output FILE (specify the file to output for no-upload mode)" echo " --update (check server for script updates)" echo " --upload (upload contents to remote server)" echo " --no-upload (do not upload contents to remote server)" echo " --pastebin (use http://pastebin.ca) as remote server" echo " instead www.alsa-project.org" echo " --stdout (print alsa information to standard output" echo " instead of a file)" echo " --about (show some information about the script)" echo " --debug (will run the script as normal, but will not" echo " delete $FILE)" exit 0 ;; esac shift 1 done fi if [ "$PROCEED" = "no" ]; then exit 1 fi if [ "$UPLOAD" = "ask" ]; then if [[ -n "$DIALOG" ]]; then dialog --backtitle "$BGTITLE" --title "Information collected" --yes-label " UPLOAD / SHARE " --no-label " SAVE LOCALLY " --defaultno --yesno "\n\nAutomatically upload ALSA information to $WWWSERVICE?" 10 80 DIALOG_EXIT_CODE=$? if [ $DIALOG_EXIT_CODE != 0 ]; then UPLOAD="no" else UPLOAD="yes" fi else echo -n "Automatically upload ALSA information to $WWWSERVICE? [y/N] : " read -e CONFIRM if [ "$CONFIRM" != "y" ]; then UPLOAD="no" else UPLOAD="yes" fi fi fi if [ "$UPLOAD" = "no" ]; then if [ -z "$TOSTDOUT" ]; then mv -f $FILE $NFILE || exit 1 KEEP_OUTPUT="yes" fi if [[ -n $DIALOG ]] then if [[ -n $PBERROR ]]; then dialog --backtitle "$BGTITLE" --title "Information collected" --msgbox "An error occurred while contacting the $WWWSERVICE.\n Your information was NOT automatically uploaded.\n\nYour ALSA information is in $NFILE" 10 100 else dialog --backtitle "$BGTITLE" --title "Information collected" --msgbox "\n\nYour ALSA information is in $NFILE" 10 60 fi else echo if [[ -n $PBERROR ]]; then echo "An error occurred while contacting the $WWWSERVICE." echo "Your information was NOT automatically uploaded." echo "" echo "Your ALSA information is in $NFILE" echo "" else if [ -z "$TOSTDOUT" ]; then echo "" echo "Your ALSA information is in $NFILE" echo "" fi fi fi exit fi # UPLOAD #Test that wget is installed, and supports --post-file. Upload $FILE if it does, and prompt user to upload file if it doesnt. if WGET=$(which wget 2>/dev/null| sed 's|^[^/]*||' 2>/dev/null); [[ -n "${WGET}" ]] && [[ -x "${WGET}" ]] && [[ `wget --help |grep post-file` ]] then if [[ -n $DIALOG ]] then if [[ -z $PASTEBIN ]]; then wget -O - --tries=5 --timeout=60 --post-file=$FILE "http://www.alsa-project.org/cardinfo-db/" &>$TEMPDIR/wget.tmp || echo "Upload failed; exit" { for i in 10 20 30 40 50 60 70 80 90; do echo $i sleep 0.2 done echo; } |dialog --backtitle "$BGTITLE" --guage "Uploading information to www.alsa-project.org ..." 6 70 0 else wget -O - --tries=5 --timeout=60 --post-file=$FILE "http://pastebin.ca/quiet-paste.php?api=$PASTEBINKEY&encrypt=t&encryptpw=blahblah" &>$TEMPDIR/wget.tmp || echo "Upload failed; exit" { for i in 10 20 30 40 50 60 70 80 90; do echo $i sleep 0.2 done echo; } |dialog --backtitle "$BGTITLE" --guage "Uploading information to www.pastebin.ca ..." 6 70 0 fi dialog --backtitle "$BGTITLE" --title "Information uploaded" --yesno "Would you like to see the uploaded information?" 5 100 DIALOG_EXIT_CODE=$? if [ $DIALOG_EXIT_CODE = 0 ]; then grep -v "alsa-info.txt" $FILE >$TEMPDIR/uploaded.txt dialog --backtitle "$BGTITLE" --textbox $TEMPDIR/uploaded.txt 0 0 fi clear # no dialog else if [[ -z $PASTEBIN ]]; then echo -n "Uploading information to www.alsa-project.org ... " wget -O - --tries=5 --timeout=60 --post-file=$FILE http://www.alsa-project.org/cardinfo-db/ &>$TEMPDIR/wget.tmp & else echo -n "Uploading information to www.pastebin.ca ... " wget -O - --tries=5 --timeout=60 --post-file=$FILE http://pastebin.ca/quiet-paste.php?api=$PASTEBINKEY &>$TEMPDIR/wget.tmp & fi #Progess spinner for wget transfer. i=1 sp="/-\|" echo -n ' ' while pgrep wget &>/dev/null do echo -en "\b${sp:i++%${#sp}:1}" done echo -e "\b Done!" echo "" fi #dialog #See if tput is available, and use it if it is. if [[ -n "$TPUT" ]] then if [[ -z $PASTEBIN ]]; then FINAL_URL=`tput setaf 1; grep "SUCCESS:" $TEMPDIR/wget.tmp | cut -d ' ' -f 2 ; tput sgr0` else FINAL_URL=`tput setaf 1; grep "SUCCESS:" $TEMPDIR/wget.tmp |sed -n 's/.*\:\([0-9]\+\).*/http:\/\/pastebin.ca\/\1/p';tput sgr0` fi else if [[ -z $PASTEBIN ]]; then FINAL_URL=`grep "SUCCESS:" $TEMPDIR/wget.tmp | cut -d ' ' -f 2` else FINAL_URL=`grep "SUCCESS:" $TEMPDIR/wget.tmp |sed -n 's/.*\:\([0-9]\+\).*/http:\/\/pastebin.ca\/\1/p'` fi fi #Output the URL of the uploaded file. echo "Your ALSA information is located at $FINAL_URL" echo "Please inform the person helping you." echo "" #We couldnt find a suitable wget, so tell the user to upload manually. else mv -f $FILE $NFILE || exit 1 KEEP_OUTPUT="yes" if [[ -z $DIALOG ]] then if [[ -z $PASTEBIN ]]; then echo "" echo "Could not automatically upload output to http://www.alsa-project.org" echo "Possible reasons are:" echo " 1. Couldnt find 'wget' in your PATH" echo " 2. Your version of wget is less than 1.8.2" echo "" echo "Please manually upload $NFILE to http://www.alsa-project.org/cardinfo-db/ and submit your post." echo "" else echo "" echo "Could not automatically upload output to http://www.pastebin.ca" echo "Possible reasons are:" echo " 1. Couldnt find 'wget' in your PATH" echo " 2. Your version of wget is less than 1.8.2" echo "" echo "Please manually upload $NFILE to http://www.pastebin.ca/upload.php and submit your post." echo "" fi else if [[ -z $PASTEBIN ]]; then dialog --backtitle "$BGTITLE" --msgbox "Could not automatically upload output to http://www.alsa-project.org.\nPossible reasons are:\n\n 1. Couldn't find 'wget' in your PATH\n 2. Your version of wget is less than 1.8.2\n\nPlease manually upload $NFILE to http://www.alsa-project,org/cardinfo-db/ and submit your post." 25 100 else dialog --backtitle "$BGTITLE" --msgbox "Could not automatically upload output to http://www.pastebin.ca.\nPossible reasons are:\n\n 1. Couldn't find 'wget' in your PATH\n 2. Your version of wget is less than 1.8.2\n\nPlease manually upload $NFILE to http://www.pastebin.ca/upload.php and submit your post." 25 100 fi fi fi 2013.com.canonical.certification.checkbox-0.4/bin/ipmi_test0000775000175000017500000000266412320541306023523 0ustar zygazyga00000000000000#!/bin/bash # Now make sure the modules are loaded for module in ipmi_si ipmi_devintf ipmi_msghandler; do if lsmod |grep -q $module; then echo "$module already loaded" else echo "Loading $module..." modprobe $module result=$? # if ipmi_si fails to load, it's safe to assume the system # has no BMC, so we'll just politely exit. if [ $result -eq 1 ] && [ "$module" = "ipmi_si" ]; then echo "WARNING: No BMC found. Aborting." exit 0 elif [ $result -eq 1 ]; then echo "ERROR: Unable to load module $module" >&2 echo "Aborting IPMI test run." >&2 exit 1 else echo "Successfully loaded module $module" fi fi done # Now get our info from ipmitool to make sure communication works # First lest check chassis status echo "Checking for chassis status" ipmitool chassis status && echo "Successfully got chassis status" && chassis=0 || chassis=1 echo "Checking to see if we can get sensor data" ipmitool sdr list full && echo "Successfully got sensor data" && sensor=0 || sensor=1 echo "Checking to see if we can get info on the BMC" ipmitool bmc info && echo "Successfully got BMC information" && bmc=0 || bmc=1 # if everything passes, exit 0 [ $chassis -eq 0 ] && [ $sensor -eq 0 ] && [ $bmc -eq 0 ] && exit 0 || echo "FAILURE: chassis: $chassis sensor: $sensor bmc: $bmc" # otherwise exit 1 exit 1 2013.com.canonical.certification.checkbox-0.4/bin/gpu_test0000775000175000017500000001604212320567463023367 0ustar zygazyga00000000000000#!/usr/bin/python3 # Copyright 2013 Canonical Ltd. # Written by: # Sylvain Pineau # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Script checking gpu lockups. Several threads are started to exercise the GPU in ways that can cause gpu lockups. Inspired by the workload directory of the xdiagnose package. """ import os import re import subprocess import sys import time from gi.repository import Gio from math import cos, sin from threading import Thread class GlxThread(Thread): """ Start a thread running glxgears """ def run(self): try: self.process = subprocess.Popen( ["glxgears","-geometry", "400x400"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) self.process.communicate() except (subprocess.CalledProcessError, FileNotFoundError) as er: print("WARNING: Unable to start glxgears (%s)" % er) def terminate(self): if not hasattr(self, 'id'): print("WARNING: Attempted to terminate non-existing window.") if hasattr(self, 'process'): self.process.terminate() class RotateGlxThread(Thread): """ Start a thread performing glxgears windows rotations """ def __init__(self, id, offset): Thread.__init__(self) self.id = id self.offset = offset self.cancel = False def run(self): while(1): for j in range(60): x = int(200 * self.offset + 100 * sin(j * 0.2)) y = int(200 * self.offset + 100 * cos(j * 0.2)) coords = "%s,%s" % (x, y) subprocess.call( 'wmctrl -i -r %s -e 0,%s,-1,-1' % (self.id, coords), shell=True ) time.sleep(0.002 * self.offset) if self.cancel: return class ChangeWorkspace(Thread): """ Start a thread performing fast workspace switches """ def __init__(self, hsize, vsize, xsize, ysize): Thread.__init__(self) self.hsize = hsize self.vsize = vsize self.xsize = xsize self.ysize = ysize self.cancel = False def run(self): while(1): for i in range(self.hsize): for j in range(self.vsize): subprocess.call( 'wmctrl -o %s,%s' % (self.xsize * j, self.ysize * i), shell=True) time.sleep(0.5) if self.cancel: # Switch back to workspace #1 subprocess.call('wmctrl -o 0,0', shell=True) return class Html5VideoThread(Thread): """ Start a thread performing playback of an HTML5 video in firefox """ @property def html5_path(self): if os.getenv('PLAINBOX_PROVIDER_DATA'): return os.path.join( os.getenv('PLAINBOX_PROVIDER_DATA'), 'websites/html5_video.html') def run(self): if self.html5_path and os.path.isfile(self.html5_path): subprocess.call( 'firefox %s' % self.html5_path, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT, shell=True) else: print("WARNING: unable to start html5 video playback.") print("WARNING: test results may be invalid.") def terminate(self): if self.html5_path and os.path.isfile(self.html5_path): subprocess.call("pkill firefox", shell=True) def check_gpu(log=None): if not log: log = '/var/log/kern.log' with open(log, 'rb') as f: if re.findall(r'gpu\s+hung', str(f.read()), flags=re.I): print("GPU hung Detected") return 1 def main(): if check_gpu(): return 1 GlxWindows = [] GlxRotate = [] subprocess.call("pkill 'glxgears|firefox'", shell=True) Html5Video = Html5VideoThread() Html5Video.start() source = Gio.SettingsSchemaSource.get_default() for i in range(2): GlxWindows.append(GlxThread()) GlxWindows[i].start() time.sleep(5) try: windows = subprocess.check_output( 'wmctrl -l | grep glxgears', shell=True) except subprocess.CalledProcessError as er: print("WARNING: Got an exception %s" % er) windows = "" for app in sorted(windows.splitlines(), reverse=True): if not b'glxgears' in app: continue GlxWindows[i].id = str( re.match(b'^(0x\w+)', app).group(0), 'utf-8') break if hasattr(GlxWindows[i], "id"): rotator = RotateGlxThread(GlxWindows[i].id, i + 1) GlxRotate.append(rotator) rotator.start() else: print("WARNING: Window {} not found, not rotating it.".format(i)) hsize = vsize = 2 hsize_ori = vsize_ori = None if source.lookup("org.compiz.core", True): settings = Gio.Settings( "org.compiz.core", "/org/compiz/profiles/unity/plugins/core/" ) hsize_ori = settings.get_int("hsize") vsize_ori = settings.get_int("vsize") settings.set_int("hsize", hsize) settings.set_int("vsize", vsize) time.sleep(5) else: hsize = int(subprocess.check_output( 'gconftool --get /apps/compiz-1/general/screen0/options/hsize', shell=True)) vsize = int(subprocess.check_output( 'gconftool --get /apps/compiz-1/general/screen0/options/vsize', shell=True)) (x_res, y_res) = re.search( b'DG:\s+(\d+)x(\d+)', subprocess.check_output('wmctrl -d', shell=True)).groups() DesktopSwitch = ChangeWorkspace( hsize, vsize, int(x_res) // hsize, int(y_res) // vsize) DesktopSwitch.start() time.sleep(35) for i in range(len(GlxRotate)): GlxRotate[i].cancel = True for i in range(len(GlxWindows)): GlxWindows[i].terminate() DesktopSwitch.cancel = True time.sleep(10) Html5Video.terminate() if check_gpu() or not Html5Video.html5_path: return 1 if source.lookup("org.compiz.core", True): settings = Gio.Settings( "org.compiz.core", "/org/compiz/profiles/unity/plugins/core/") settings.set_int("hsize", hsize_ori) settings.set_int("vsize", vsize_ori) Gio.Settings.sync() if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/optical_detect0000775000175000017500000000115012320541306024476 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import re import sys def main(args): line_pattern = r"\s*(\d+)\s+dev='([^']+)'" \ "\s+([wr-]{6})\s:\s'([^']+)'\s'([^']+)'" line_regex = re.compile(line_pattern) count = 0 command = "wodim --devices" for line in os.popen(command).readlines(): match = re.match(line_regex, line) if match: count += 1 print(match.group(4), match.group(5)) if not count: print("No optical devices detected.", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/led_hdd_test0000775000175000017500000000056312320541306024144 0ustar zygazyga00000000000000#!/bin/bash TIMEOUT=3 TEMPFILE=`mktemp` trap "rm $TEMPFILE" EXIT for i in $(seq $TIMEOUT); do #launch background writer dd if=/dev/urandom of=$TEMPFILE bs=1024 oflag=direct & WRITE_PID=$! echo "Writing..." sleep 1 kill $WRITE_PID sync echo "Reading..." dd if=$TEMPFILE of=/dev/null bs=1024 iflag=direct done echo "OK, now exiting" 2013.com.canonical.certification.checkbox-0.4/bin/window_test0000775000175000017500000002433712320541306024075 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # window_test # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import threading import time import os import sys from signal import SIGTSTP, SIGCONT, SIGTERM from subprocess import check_call, check_output, Popen, PIPE from argparse import ArgumentParser class AppThread(threading.Thread): def __init__(self, app_name): self._appname = app_name self.stdout = None self.stderr = None self.pid = None threading.Thread.__init__(self) def run(self): proc = Popen(self._appname, stdout=PIPE, stderr=PIPE) self.pid = proc.pid print(' Starting "%s", PID: %d' % (self._appname, self.pid)) self.stdout, self.stderr = proc.communicate() def open_close_process(app, timeout): '''Open and close a process after a timeout''' status = 0 # Start the process in a separate thread app_thread = AppThread(app) app_thread.start() # Wait until we have a pid while app_thread.pid is None: continue pid = app_thread.pid # Wait a bit and kill the process time.sleep(timeout) print(' Killing "%s", PID: %d' % (app, pid)) os.kill(pid, SIGTERM) if app_thread.stderr: print('Errors:\n%s' % app_thread.stderr, file=sys.stderr) status = 1 time.sleep(timeout) return status def open_close_multi_process(app, timeout, apps_num): '''Open and close multiple processes after a timeout''' status = 0 threads = [] for thread in range(apps_num): app_thread = AppThread(app) app_thread.start() threads.append(app_thread) for thread in threads: # Wait until we have a pid while thread.pid is None: continue # Wait a bit and kill the process time.sleep(timeout) for thread in threads: print(' Killing "%s", PID: %d' % (app, thread.pid)) os.kill(thread.pid, SIGTERM) if thread.stderr: print('Errors:\n%s' % thread.stderr, file=sys.stderr) status = 1 time.sleep(timeout) return status def open_suspend_close_process(app, timeout): '''Open, suspend and close a process after a timeout''' status = 0 # Start the process in a separate thread app_thread = AppThread(app) app_thread.start() # Wait until we have a pid while app_thread.pid is None: continue pid = app_thread.pid # Wait a bit and suspend the process time.sleep(timeout) print(' Suspending "%s", PID: %d' % (app, pid)) os.kill(pid, SIGTSTP) # Wait a bit and resume the process time.sleep(timeout) print(' Resuming "%s", PID: %d' % (app, pid)) os.kill(pid, SIGCONT) # Wait a bit and kill the process time.sleep(timeout) print(' Killing "%s", PID: %d' % (app, pid)) os.kill(pid, SIGTERM) if app_thread.stderr: print('Errors:\n%s' % app_thread.stderr, file=sys.stderr) status = 1 time.sleep(timeout) return status def move_window(app, timeout): status = 0 # Start the process in a separate thread app_thread = AppThread(app) app_thread.start() while app_thread.pid is None: continue pid = app_thread.pid time.sleep(3) window_list = check_output(['wmctrl', '-l'], universal_newlines=True) window_id = '' for line in window_list.split('\n'): if app in line: window_id = line.split()[0] if window_id: # Get the screen information from GDK from gi.repository import Gdk screen = Gdk.Screen.get_default() geom = screen.get_monitor_geometry(screen.get_primary_monitor()) # Find out the window information from xwininfo win_x = '' win_y = '' win_width = '' win_height = '' for line in check_output(['xwininfo', '-name', app], universal_newlines=True).split('\n'): if 'Absolute upper-left X' in line: win_x = line.split(': ')[-1].strip() elif 'Absolute upper-left Y' in line: win_y = line.split(': ')[-1].strip() elif 'Width' in line: win_width = line.split(': ')[-1].strip() elif 'Height' in line: win_height = line.split(': ')[-1].strip() move_line = ["0", win_x, win_y, win_width, win_height] directions = {'RIGHT': geom.width, 'DOWN': geom.height, 'LEFT': win_x, 'UP': win_y, 'STOP': None} current = 'RIGHT' while current != 'STOP': if current == 'RIGHT': # Check if top right corner of window reached top right point if int(move_line[1]) + int(win_width) != directions[current]: new_x = int(move_line[1]) + 1 move_line[1] = str(new_x) else: current = 'DOWN' elif current == 'DOWN': if int(move_line[2]) + int(win_height) != directions[current]: new_y = int(move_line[2]) + 1 move_line[2] = str(new_y) else: current = 'LEFT' elif current == 'LEFT': if int(move_line[1]) != int(directions[current]): new_x = int(move_line[1]) - 1 move_line[1] = str(new_x) else: current = 'UP' elif current == 'UP': if int(move_line[2]) != int(directions[current]): new_y = int(move_line[2]) - 1 move_line[2] = str(new_y) else: current = 'STOP' check_call(['wmctrl', '-i', '-r', window_id, '-e', ','.join(move_line)]) os.kill(pid, SIGTERM) else: print("Could not get window handle for %s" % app, file=sys.stderr) status = 1 return status def print_open_close(iterations, timeout, *args): status = 0 print('Opening and closing a 3D window') for it in range(iterations): print('Iteration %d of %d:' % (it + 1, iterations)) exit_status = open_close_process('glxgears', timeout) if exit_status != 0: status = 1 print('') return status def print_suspend_resume(iterations, timeout, *args): status = 0 print('Opening, suspending, resuming and closing a 3D window') for it in range(iterations): print('Iteration %d of %d:' % (it + 1, iterations)) exit_status = open_suspend_close_process('glxgears', timeout) if exit_status != 0: status = 1 print('') return status def print_open_close_multi(iterations, timeout, windows_number): status = 0 print('Opening and closing %d 3D windows at the same time' % windows_number) for it in range(iterations): print('Iteration %d of %d:' % (it + 1, iterations)) exit_status = open_close_multi_process('glxgears', timeout, windows_number) if exit_status != 0: status = 1 print('') return status def print_move_window(iterations, timeout, *args): status = 0 print('Moving a 3D window across the screen') for it in range(iterations): print('Iteration %d of %d:' % (it + 1, iterations)) exit_status = move_window('glxgears', timeout) print('') return status def main(): tests = {'open-close': print_open_close, 'suspend-resume': print_suspend_resume, 'open-close-multi': print_open_close_multi, 'move': print_move_window} parser = ArgumentParser(description='Script that performs window operation') parser.add_argument('-t', '--test', default='all', help='The name of the test to run. \ Available tests: \ %s, all. \ Default is all' % (', '.join(tests))) parser.add_argument('-i', '--iterations', type=int, default=1, help='The number of times to run the test. \ Default is 1') parser.add_argument('-a', '--application', default='glxgears', help='The 3D application to launch. \ Default is "glxgears"') parser.add_argument('-to', '--timeout', type=int, default=3, help='The time in seconds between each test. \ Default is 3') parser.add_argument('-w', '--windows-number', type=int, default=4, help='The number of windows to open.') args = parser.parse_args() status = 0 test = tests.get(args.test) if test: status = test(args.iterations, args.timeout, args.windows_number) else: if args.test == 'all': for test in tests: exit_status = tests[test](args.iterations, args.timeout, args.windows_number) if exit_status != 0: status = exit_status else: parser.error('-t or --test can only be used with one ' 'of the following tests: ' '%s, all' % (', '.join(tests))) return status if __name__ == '__main__': exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/xml_sanitize0000775000175000017500000000257612320541306024236 0ustar zygazyga00000000000000#!/usr/bin/python3 import errno import io import sys from argparse import ArgumentParser, FileType VALID_XML_CHARS = frozenset([0x9, 0xA, 0xD] + list(range(0x20, 0xD7FF)) + list(range(0xE000, 0xFFFD)) + list(range(0x10000, 0x10FFFF))) def is_valid_xml_char(ch): # Is this character valid in XML? # http://www.w3.org/TR/xml/#charsets return ord(ch) in VALID_XML_CHARS def main(): parser = ArgumentParser("Receives as input some text and outputs " "the same text without characters which are " "not valid in the XML specification.") parser.add_argument('input_file', type=FileType('r'), nargs='?', help='The name of the file to sanitize.') args = parser.parse_args() if args.input_file: text = ''.join([c for c in args.input_file.read() if is_valid_xml_char(c)]) else: with io.TextIOWrapper( sys.stdin.buffer, encoding='UTF-8', errors="ignore") as stdin: text = ''.join([c for c in stdin.read() if is_valid_xml_char(c)]) print(text) if __name__ == "__main__": try: sys.exit(main()) except Exception as err: if err.errno != errno.EPIPE: raise(err) 2013.com.canonical.certification.checkbox-0.4/bin/xrandr_cycle0000775000175000017500000001354112320541306024177 0ustar zygazyga00000000000000#!/usr/bin/env python3 import subprocess import argparse import tarfile import shutil import time import sys import os import re parser = argparse.ArgumentParser() parser.add_argument('--keyword', default='', help=('A keyword to distinguish the screenshots ' 'taken in this run of the script')) parser.add_argument('--screenshot-dir', default=os.environ['HOME'], help=('Specify a directory to store screenshots in. ' 'Default is %(default)s')) args = parser.parse_args() device_context = '' # track what device's modes we are looking at modes = [] # keep track of all the devices and modes discovered current_modes = [] # remember the user's current settings for cleanup later failures = 0 # count the number of failed modesets failure_messages = [] # remember which modes failed success_messages = [] # remember which modes succeeded # Run xrandr and ask it what devices and modes are supported xrandrinfo = subprocess.Popen('xrandr -q', shell=True, stdout=subprocess.PIPE) output = xrandrinfo.communicate()[0].decode().split('\n') # The results from xrandr are given in terms of the available display devices. # One device can have zero or more associated modes. Unfortunately xrandr # indicates this through indentation and is kinda wordy, so we have to keep # track of the context we see mode names in as we parse the results. for line in output: # I haven't seen any blank lines in xrandr's output in my tests, but meh if line == '': break # luckily the various data from xrandr are separated by whitespace... foo = line.split() # Check to see if the second word in the line indicates a new context # -- if so, keep track of the context of the device we're seeing if len(foo) >= 2: # throw out any weirdly formatted lines if foo[1] == 'disconnected': # we have a new context, but it should be ignored device_context = '' if foo[1] == 'connected': # we have a new context that we want to test device_context = foo[0] elif device_context != '': # we've previously seen a 'connected' dev # mode names seem to always be of the format [horiz]x[vert] # (there can be non-mode information inside of a device context!) if foo[0].find('x') != -1: modes.append((device_context, foo[0])) # we also want to remember what the current mode is, which xrandr # marks with a '*' character, so we can set things back the way # we found them at the end: if foo[1].find('*') != -1: current_modes.append((device_context, foo[0])) # Now we have a list of the modes we need to test. So let's do just that. profile_path = os.environ['HOME'] + '/.shutter/profiles/' screenshot_path = os.path.join(args.screenshot_dir, 'xrandr_screens') script_home = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] if args.keyword: screenshot_path = screenshot_path + '_' + args.keyword regex = re.compile(r'filename="[^"\r\n]*"') # Keep the shutter profile in place before starting try: os.makedirs(profile_path) except OSError: pass try: os.makedirs(screenshot_path) except OSError: pass try: shutil.copy(script_home + '/data/settings/shutter.xml', profile_path) except IOError: pass try: old_profile = open(profile_path + 'shutter.xml', 'r') content = old_profile.read() new_profile = open(profile_path + 'shutter.xml', 'w') # Replace the folder name with the desired one new_profile.write(re.sub(r'folder="[^"\r\n]*"', 'folder="%s"' % screenshot_path, content)) new_profile.close() old_profile.close() except: pass for mode in modes: cmd = 'xrandr --output ' + mode[0] + ' --mode ' + mode[1] retval = subprocess.call(cmd, shell=True) if retval != 0: failures = failures + 1 message = 'Failed to set mode ' + mode[1] + ' for output ' + mode[0] failure_messages.append(message) else: # Update shutter profile to save the image as the right name mode_string = mode[0] + '_' + mode[1] try: old_profile = open(profile_path + 'shutter.xml', 'r') content = old_profile.read() new_profile = open(profile_path + 'shutter.xml', 'w') new_profile.write(regex.sub('filename="%s"' % mode_string, content)) new_profile.close() old_profile.close() shuttercmd = ['shutter', '--profile=shutter', '--full', '-e'] retval = subprocess.call(shuttercmd, shell=False) if retval != 0: print("""Could not capture screenshot - you may need to install the package 'shutter'.""") except: print("""Could not configure screenshot tool - you may need to install the package 'shutter', or check that %s exists.""" % profile_path) message = 'Set mode ' + mode[1] + ' for output ' + mode[0] success_messages.append(message) time.sleep(3) # let the hardware recover a bit # Put things back the way we found them for mode in current_modes: cmd = 'xrandr --output ' + mode[0] + ' --mode ' + mode[1] subprocess.call(cmd, shell=True) # Tar up the screenshots for uploading try: with tarfile.open(screenshot_path + '.tgz', 'w:gz') as screen_tar: for screen in os.listdir(screenshot_path): screen_tar.add(screenshot_path + '/' + screen, screen) except: pass # Output some fun facts and knock off for the day for message in failure_messages: print(message, file=sys.stderr) for message in success_messages: print(message) if failures != 0: exit(1) else: exit(0) 2013.com.canonical.certification.checkbox-0.4/bin/pm_log_check0000775000175000017500000002050412320541306024131 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import re import difflib import logging from argparse import ArgumentParser # Script return codes SUCCESS = 0 NOT_MATCH = 1 NOT_FINISHED = 2 NOT_FOUND = 3 def main(): args = parse_args() if not os.path.isfile(args.input_log_filename): sys.stderr.write('Log file {0!r} not found\n' .format(args.input_log_filename)) sys.exit(NOT_FOUND) LoggingConfiguration.set(args.log_level, args.output_log_filename) parser = Parser(args.input_log_filename) results = parser.parse() if not compare_results(results): sys.exit(NOT_MATCH) sys.exit(SUCCESS) class Parser(object): """ Reboot test log file parser """ is_logging_line = (re.compile('^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}') .search) is_getting_info_line = (re.compile('Gathering hardware information...$') .search) is_executing_line = (re.compile("Executing: '(?P.*)'...$") .search) is_output_line = re.compile('Output:$').search is_field_line = (re.compile('^- (?Preturncode|stdout|stderr):$') .match) is_test_complete_line = re.compile('test complete$').search def __init__(self, filename): self.filename = filename def parse(self): """ Parse log file and return results """ with open(self.filename) as f: results = self._parse_file(LineIterator(f)) return results def _parse_file(self, iterator): """ Parse all lines in iterator and return results """ results = [] result = {} for line in iterator: if self.is_getting_info_line(line): if result: # Add last result to list of results results.append(result) # Initialize for a new iteration results result = {} match = self.is_executing_line(line) if match: command = match.group('command') command_output = self._parse_command_output(iterator) if command_output is not None: result[command] = command_output else: if result: # Add last result to list of results results.append(result) if not self.is_test_complete_line(line): sys.stderr.write("Test didn't finish properly according to logs\n") sys.exit(NOT_FINISHED) return results def _parse_command_output(self, iterator): """ Parse one command output """ command_output = None # Skip all lines until command output is found for line in iterator: if self.is_output_line(line): command_output = {} break if (self.is_executing_line(line) or self.is_getting_info_line(line)): # Skip commands with no output iterator.unnext(line) return None # Parse command output message for line in iterator: match = self.is_field_line(line) if match: field = match.group('field') value = self._parse_command_output_field(iterator) command_output[field] = value # Exit when all command output fields # have been gathered else: iterator.unnext(line) break return command_output def _parse_command_output_field(self, iterator): """ Parse one command output field """ # Accummulate as many lines as needed # for the field value value = [] for line in iterator: if (self.is_logging_line(line) or self.is_field_line(line)): iterator.unnext(line) break value.append(line) value = ''.join(value) return value class LineIterator: """ Iterator wrapper to make it possible to push back lines that shouldn't have been consumed """ def __init__(self, iterator): self.iterator = iterator self.buffer = [] def __iter__(self): return self def __next__(self): if self.buffer: return self.buffer.pop() return next(self.iterator) def unnext(self, line): self.buffer.append(line) class LoggingConfiguration(object): @classmethod def set(cls, log_level, log_filename): """ Configure a rotating file logger """ logger = logging.getLogger() logger.setLevel(logging.DEBUG) # Log to sys.stderr using log level passed through command line if log_level != logging.NOTSET: log_handler = logging.StreamHandler() formatter = logging.Formatter('%(levelname)-8s %(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(log_level) logger.addHandler(log_handler) # Log to rotating file using DEBUG log level log_handler = logging.FileHandler(log_filename, mode='w') formatter = logging.Formatter('%(asctime)s %(levelname)-8s ' '%(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(logging.DEBUG) logger.addHandler(log_handler) def compare_results(results): """ Compare results using first one as a baseline """ baseline = results[0] success = True for index, result in enumerate(results[1:]): for command in baseline.keys(): baseline_output = baseline[command] result_output = result[command] error_messages = [] fields = (set(baseline_output.keys()) | set(result_output.keys())) for field in fields: baseline_field = baseline_output.get(field, '') result_field = result_output.get(field, '') if baseline_field != result_field: differ = difflib.Differ() message = ["** {field!r} field doesn't match:" .format(field=field)] comparison = differ.compare(baseline_field.splitlines(), result_field.splitlines()) message.extend(list(comparison)) error_messages.append('\n'.join(message)) if not error_messages: logging.debug('[Iteration {0}] {1}...\t[OK]' .format(index + 1, command)) else: success = False if command.startswith('fwts'): logging.error('[Iteration {0}] {1}...\t[FAIL]' .format(index + 1, command)) else: logging.error('[Iteration {0}] {1}...\t[FAIL]\n' .format(index + 1, command)) for message in error_messages: logging.error(message) return success def parse_args(): """ Parse command-line arguments """ parser = ArgumentParser(description=('Check power management ' 'test case results')) parser.add_argument('input_log_filename', metavar='log_filename', help=('Path to the input log file ' 'on which to perform the check')) parser.add_argument('output_log_filename', metavar='log_filename', help=('Path to the output log file ' 'for the results of the check')) log_levels = ['notset', 'debug', 'info', 'warning', 'error', 'critical'] parser.add_argument('--log-level', dest='log_level', default='info', choices=log_levels, help=('Log level. ' 'One of {0} or {1} (%(default)s by default)' .format(', '.join(log_levels[:-1]), log_levels[-1]))) args = parser.parse_args() args.log_level = getattr(logging, args.log_level.upper()) return args if __name__ == '__main__': main() 2013.com.canonical.certification.checkbox-0.4/bin/cpu_offlining0000775000175000017500000000241212320541306024337 0ustar zygazyga00000000000000#!/bin/bash echo "Beginning CPU Offlining Test" 1>&2 result=0 cpu_count=0 # Turn CPU cores off for cpu_num in `ls /sys/devices/system/cpu | grep -o cpu[0-9]*`; do if [ -f /sys/devices/system/cpu/$cpu_num/online ]; then if [ "$cpu_num" != "cpu0" ]; then ((cpu_count++)) echo "Offlining $cpu_num" 1>&2 echo 0 > /sys/devices/system/cpu/$cpu_num/online grep -w -i -q $cpu_num /proc/interrupts if [ $? -eq 0 ]; then echo "ERROR: Failed to offline $cpu_num" 1>&2 result=1 fi fi fi done # Back on again for cpu_num in `ls /sys/devices/system/cpu | grep -o cpu[0-9]*`; do if [ -f /sys/devices/system/cpu/$cpu_num/online ]; then if [ "$cpu_num" != "cpu0" ]; then echo "Onlining $cpu_num" 1>&2 echo 1 > /sys/devices/system/cpu/$cpu_num/online grep -w -i -q $cpu_num /proc/interrupts if [ $? -eq 1 ]; then echo "ERROR: Failed to online $cpu_num" 1>&2 result=1 fi fi fi done if [ $result -eq 0 ]; then echo "Successfully turned $cpu_count cores off and back on" else echo "Error with offlining one or more cores." 1>&2 fi exit $result 2013.com.canonical.certification.checkbox-0.4/bin/fresh_rate_info0000775000175000017500000000417512320541306024662 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # fresh_rate_info # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Shawn Wang # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . """ The fresh_rate_info got information from xrandr """ import re import sys import subprocess def xrandr_paser(data=None): '''return an array(xrandrs)''' resolution = None xrandrs = list() for line in str(data).split('\n'): for match in re.finditer('(.+) connected (\d+x\d+)\+', line): connector = match.group(1) resolution = match.group(2) break if resolution is None: continue for match in re.finditer('{0}\s+(.+)\*'.format(resolution), line): refresh_rate = match.group(1) xrandr = {'connector': connector, 'resolution': resolution, 'refresh_rate': refresh_rate} xrandrs.append(xrandr) return xrandrs def main(): '''main function''' try: data = subprocess.check_output(['xrandr', '--current'], universal_newlines=True) except subprocess.CalledProcessError as exc: return exc.returncode xrandrs = xrandr_paser(data) for xrandr in xrandrs: output_str = "Connector({0}):\t Resolution: {1} \t RefreshRate: {2}" print(output_str.format(xrandr['connector'], xrandr['resolution'], xrandr['refresh_rate'])) if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/touchpad_test0000775000175000017500000001372312320541306024372 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys import gettext from gettext import gettext as _ from gi.repository import Gio, Gtk, Gdk from optparse import OptionParser EXIT_WITH_FAILURE = 1 EXIT_WITH_SUCCESS = 0 EXIT_TIMEOUT = 30 class Direction(object): def __init__(self, name): self.name = name self.tested = False self.value = getattr(Gdk.ScrollDirection, name.upper()) class GtkScroller(object): touchpad_key = "org.gnome.settings-daemon.peripherals.touchpad" exit_code = EXIT_WITH_FAILURE def __init__(self, directions, edge_scroll=False): self.directions = directions self.edge_scroll = edge_scroll # Initialize GTK constants self.ICON_SIZE = Gtk.IconSize.BUTTON self.ICON_TESTED = Gtk.STOCK_YES self.ICON_UNTESTED = Gtk.STOCK_INDEX self.ICON_NOT_REQUIRED = Gtk.STOCK_REMOVE self.button_factory = Gtk.Button self.hbox_factory = Gtk.HBox self.image_factory = Gtk.Image self.label_factory = Gtk.Label self.vbox_factory = Gtk.VBox # Create GTK window. window = Gtk.Window() window.set_type_hint(Gdk.WindowType.TOPLEVEL) window.add_events( Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) window.set_size_request(200, 100) window.set_resizable(False) window.set_title(_("Type Text")) window.connect("delete-event", lambda w, e: self.quit()) window.connect("scroll-event", self.on_scroll) window.show() # Add common widgets to the window. vbox = self._add_vbox(window) self.label = self._add_label(vbox) button_hbox = self._add_hbox(vbox) validation_hbox = self._add_hbox(vbox) self.status = self._add_label(vbox) self.exit_button = self._add_button(vbox, Gtk.STOCK_CLOSE) self.exit_button.connect("clicked", lambda w: self.quit()) # Add widgets for each direction. self.icons = {} for direction in self.directions: self._add_label(button_hbox, direction.name) self.icons[direction] = self._add_image( validation_hbox, Gtk.STOCK_INDEX) self.show_text( _("Please move the mouse cursor to this window.") + "\n" + _("Then scroll in each direction on your touchpad.")) def _add_button(self, context, stock): button = self.button_factory(stock=stock) context.add(button) button.show() return button def _add_hbox(self, context, spacing=4): hbox = self.hbox_factory() context.add(hbox) hbox.set_spacing(4) hbox.show() return hbox def _add_image(self, context, stock): image = self.image_factory(stock=stock, icon_size=self.ICON_SIZE) context.add(image) image.show() return image def _add_label(self, context, text=None): label = self.label_factory() context.add(label) label.set_size_request(0, 0) label.set_line_wrap(True) if text: label.set_text(text) label.show() return label def _add_vbox(self, context): vbox = self.vbox_factory() vbox.set_homogeneous(False) vbox.set_spacing(8) context.add(vbox) vbox.show() return vbox def run(self): # Save touchpad settings. touchpad_settings = Gio.Settings.new(self.touchpad_key) self.saved_horiz_scroll_enabled = touchpad_settings.get_boolean( "horiz-scroll-enabled") self.saved_scroll_method = touchpad_settings.get_string( "scroll-method") # Set touchpad settings. touchpad_settings.set_boolean("horiz-scroll-enabled", True) if self.edge_scroll: touchpad_settings.set_string("scroll-method", "edge-scrolling") Gtk.main() def quit(self): # Reset touchpad settings. touchpad_settings = Gio.Settings.new(self.touchpad_key) touchpad_settings.set_boolean( "horiz-scroll-enabled", self.saved_horiz_scroll_enabled) touchpad_settings.set_string( "scroll-method", self.saved_scroll_method) Gtk.main_quit() def show_text(self, text, widget=None): if widget is None: widget = self.label widget.set_text(text) def found_direction(self, direction): direction.tested = True self.icons[direction].set_from_stock( self.ICON_TESTED, size=self.ICON_SIZE) self.check_directions() def check_directions(self): if all([direction.tested for direction in self.directions]): self.show_text( _("All required directions have been tested!"), self.status) self.exit_code = EXIT_WITH_SUCCESS self.exit_button.grab_focus() def on_scroll(self, window, event): for direction in self.directions: if direction.value == event.direction: self.found_direction(direction) break return True def main(args): gettext.textdomain("checkbox") usage = """Usage: %prog DIRECTION... [--edge-scroll]""" parser = OptionParser(usage=usage) parser.add_option("--edge-scroll", action="store_true", default=False, help="Force touchpad to use edge scrolling only") (options, args) = parser.parse_args(args) if not args: parser.error("Must specify directions to test.") directions = [] for arg in args: try: direction = Direction(arg) except AttributeError: parser.error("Unsupported direction: %s" % arg) directions.append(direction) scroller = GtkScroller(directions, edge_scroll=options.edge_scroll) try: scroller.run() except KeyboardInterrupt: scroller.show_text(_("Test interrupted"), self.status) scroller.quit() return scroller.exit_code if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/network_reconnect_resume_test0000775000175000017500000000647412320541306027701 0ustar zygazyga00000000000000#!/usr/bin/env python3 # Copyright (C) 2012 Canonical, Ltd. import re import subprocess import argparse import sys def get_time_difference(device): """ Returns the difference in seconds between the last resume from suspend (S3) and the time it took to reconnect to Wifi. If there is a problem finding the information, None is returned. """ resume_time = get_resume_time() if resume_time is None: print("Unable to obtain wakeup/resume time from dmesg." "Please be sure the system has been suspended", file=sys.stderr) return None if device == "wifi": reconnect_times = list(get_wifi_reconnect_times()) elif device == "wired": reconnect_times = list(get_wired_reconnect_times()) if not reconnect_times: print("Unable to obtain %s connection time after a S3. Please be sure" " that the system has been suspended" % device, file=sys.stderr) return None # since some wifi & wired tests can disconnect and reconnect us multiple # times after a suspend, we need to find the reconnect that occurs # immediately after the resume from S3 for reconnect_time in reconnect_times: if reconnect_time >= resume_time: return round((reconnect_time - resume_time), 2) return None def get_wifi_reconnect_times(): """ Returns a list of all the timestamps for wifi reconnects. """ data = subprocess.check_output(['dmesg'], universal_newlines=True) syntax = re.compile("\[(.*)\] wlan.* associated") results = re.findall(syntax, data) return map(float, results) def get_wired_reconnect_times(): """ Returns a list of all the timestamps for wired reconnects. """ data = subprocess.check_output(['dmesg'], universal_newlines=True) syntax = re.compile("\[(.*)\].*eth.* Link is [uU]p") results = re.findall(syntax, data) return map(float, results) def get_resume_time(): """ Returns the last (most recent) timestamp for an ACPI resume from sleep (S3) If no resume is found, None is returned. """ data = subprocess.check_output(['dmesg'], universal_newlines=True) syntax = re.compile("\[(.*)\].ACPI: Waking up from system sleep state S3") results = re.findall(syntax, data) if not results: return None else: return float(results[-1]) def main(): parser = argparse.ArgumentParser() parser.add_argument('-t', '--timeout', type=int, help="Specified max time allowed for Wifi/Wired to" " reconnect in seconds", required=True) parser.add_argument('-d', '--device', help="Specify the device to test either, eth or wlan", required=True, choices=['wifi', 'wired']) args = parser.parse_args() timedif = get_time_difference(args.device) if not timedif: return 0 print("Your %s resumed in %s seconds after the last suspend" % ( args.device, timedif)) if timedif > args.timeout: print("FAIL: the network failed to reconnect within the allotted time") return 1 else: print("PASS: the network connected within the allotted time") return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/camera_test0000775000175000017500000005062112320541306024011 0ustar zygazyga00000000000000#!/usr/bin/env python3 # # This file is part of Checkbox. # # Copyright 2008-2012 Canonical Ltd. # # The v4l2 ioctl code comes from the Python bindings for the v4l2 # userspace api (http://pypi.python.org/pypi/v4l2): # Copyright (C) 1999-2009 the contributors # # The JPEG metadata parser is a part of bfg-pages: # http://code.google.com/p/bfg-pages/source/browse/trunk/pages/getimageinfo.py # Copyright (C) Tim Hoffman # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . # import os import re import sys import time import errno import fcntl import ctypes import struct import imghdr from tempfile import NamedTemporaryFile from subprocess import check_call, CalledProcessError, STDOUT import argparse from glob import glob from gi.repository import GObject _IOC_NRBITS = 8 _IOC_TYPEBITS = 8 _IOC_SIZEBITS = 14 _IOC_NRSHIFT = 0 _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS _IOC_WRITE = 1 _IOC_READ = 2 def _IOC(dir_, type_, nr, size): return ( ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value | ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value | ctypes.c_int32(nr << _IOC_NRSHIFT).value | ctypes.c_int32(size << _IOC_SIZESHIFT).value) def _IOC_TYPECHECK(t): return ctypes.sizeof(t) def _IOR(type_, nr, size): return _IOC(_IOC_READ, type_, nr, ctypes.sizeof(size)) def _IOWR(type_, nr, size): return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) class v4l2_capability(ctypes.Structure): """ Driver capabilities """ _fields_ = [ ('driver', ctypes.c_char * 16), ('card', ctypes.c_char * 32), ('bus_info', ctypes.c_char * 32), ('version', ctypes.c_uint32), ('capabilities', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] # Values for 'capabilities' field V4L2_CAP_VIDEO_CAPTURE = 0x00000001 V4L2_CAP_VIDEO_OVERLAY = 0x00000004 V4L2_CAP_READWRITE = 0x01000000 V4L2_CAP_STREAMING = 0x04000000 v4l2_frmsizetypes = ctypes.c_uint ( V4L2_FRMSIZE_TYPE_DISCRETE, V4L2_FRMSIZE_TYPE_CONTINUOUS, V4L2_FRMSIZE_TYPE_STEPWISE, ) = range(1, 4) class v4l2_frmsize_discrete(ctypes.Structure): _fields_ = [ ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ] class v4l2_frmsize_stepwise(ctypes.Structure): _fields_ = [ ('min_width', ctypes.c_uint32), ('min_height', ctypes.c_uint32), ('step_width', ctypes.c_uint32), ('min_height', ctypes.c_uint32), ('max_height', ctypes.c_uint32), ('step_height', ctypes.c_uint32), ] class v4l2_frmsizeenum(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('discrete', v4l2_frmsize_discrete), ('stepwise', v4l2_frmsize_stepwise), ] _fields_ = [ ('index', ctypes.c_uint32), ('pixel_format', ctypes.c_uint32), ('type', ctypes.c_uint32), ('_u', _u), ('reserved', ctypes.c_uint32 * 2) ] _anonymous_ = ('_u',) class v4l2_fmtdesc(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('type', ctypes.c_int), ('flags', ctypes.c_uint32), ('description', ctypes.c_char * 32), ('pixelformat', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] V4L2_FMT_FLAG_COMPRESSED = 0x0001 V4L2_FMT_FLAG_EMULATED = 0x0002 # ioctl code for video devices VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability) VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum) VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc) class CameraTest: """ A simple class that displays a test image via GStreamer. """ def __init__(self, args, gst_plugin=None, gst_video_type=None): self.args = args self._mainloop = GObject.MainLoop() self._width = 640 self._height = 480 self._gst_plugin = gst_plugin self._gst_video_type = gst_video_type def detect(self): """ Display information regarding webcam hardware """ cap_status = dev_status = 1 for i in range(10): cp = v4l2_capability() device = '/dev/video%d' % i try: with open(device, 'r') as vd: fcntl.ioctl(vd, VIDIOC_QUERYCAP, cp) except IOError: continue dev_status = 0 print("%s: OK" % device) print(" name : %s" % cp.card.decode('UTF-8')) print(" driver : %s" % cp.driver.decode('UTF-8')) print(" version: %s.%s.%s" % (cp.version >> 16, (cp.version >> 8) & 0xff, cp.version & 0xff)) print(" flags : 0x%x [" % cp.capabilities, ' CAPTURE' if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE else '', ' OVERLAY' if cp.capabilities & V4L2_CAP_VIDEO_OVERLAY else '', ' READWRITE' if cp.capabilities & V4L2_CAP_READWRITE else '', ' STREAMING' if cp.capabilities & V4L2_CAP_STREAMING else '', ' ]', sep="") resolutions = self._get_supported_resolutions(device) print(' ', self._supported_resolutions_to_string(resolutions).replace( "\n", " "), sep="") if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE: cap_status = 0 return dev_status | cap_status def led(self): """ Activate camera (switch on led), but don't display any output """ pipespec = ("v4l2src device=%(device)s " "! %(type)s " "! %(plugin)s " "! testsink" % {'device': self.args.device, 'type': self._gst_video_type, 'plugin': self._gst_plugin}) self._pipeline = Gst.parse_launch(pipespec) self._pipeline.set_state(Gst.State.PLAYING) time.sleep(10) self._pipeline.set_state(Gst.State.NULL) def display(self): """ Displays the preview window """ pipespec = ("v4l2src device=%(device)s " "! %(type)s,width=%(width)d,height=%(height)d " "! %(plugin)s " "! autovideosink" % {'device': self.args.device, 'type': self._gst_video_type, 'width': self._width, 'height': self._height, 'plugin': self._gst_plugin}) self._pipeline = Gst.parse_launch(pipespec) self._pipeline.set_state(Gst.State.PLAYING) time.sleep(10) self._pipeline.set_state(Gst.State.NULL) def still(self): """ Captures an image to a file """ if self.args.filename: self._still_helper(self.args.filename, self._width, self._height, self.args.quiet) else: with NamedTemporaryFile(prefix='camera_test_', suffix='.jpg') as f: self._still_helper(f.name, self._width, self._height, self.args.quiet) def _still_helper(self, filename, width, height, quiet, pixelformat=None): """ Captures an image to a given filename. width and height specify the image size and quiet controls whether the image is displayed to the user (quiet = True means do not display image). """ command = ["fswebcam", "-D 1", "-S 50", "--no-banner", "-d", self.args.device, "-r", "%dx%d" % (width, height), filename] use_gstreamer = False if pixelformat: command.extend(["-p", pixelformat]) try: check_call(command, stdout=open(os.devnull, 'w'), stderr=STDOUT) except (CalledProcessError, OSError): use_gstreamer = True if use_gstreamer: pipespec = ("v4l2src device=%(device)s " "! %(type)s,width=%(width)d,height=%(height)d " "! %(plugin)s " "! jpegenc " "! filesink location=%(filename)s" % {'device': self.args.device, 'type': self._gst_video_type, 'width': width, 'height': height, 'plugin': self._gst_plugin, 'filename': filename}) self._pipeline = Gst.parse_launch(pipespec) self._pipeline.set_state(Gst.State.PLAYING) time.sleep(3) self._pipeline.set_state(Gst.State.NULL) if not quiet: try: check_call(["timeout", "-k", "11", "10", "eog", filename]) except CalledProcessError: pass def _supported_resolutions_to_string(self, supported_resolutions): """ Return a printable string representing a list of supported resolutions """ ret = "" for resolution in supported_resolutions: ret += "Format: %s (%s)\n" % (resolution['pixelformat'], resolution['description']) ret += "Resolutions: " for res in resolution['resolutions']: ret += "%sx%s," % (res[0], res[1]) # truncate the extra comma with :-1 ret = ret[:-1] + "\n" return ret def resolutions(self): """ After querying the webcam for supported formats and resolutions, take multiple images using the first format returned by the driver, and see if they are valid """ resolutions = self._get_supported_resolutions(self.args.device) # print supported formats and resolutions for the logs print(self._supported_resolutions_to_string(resolutions)) # pick the first format, which seems to be what the driver wants for a # default. This also matches the logic that fswebcam uses to select # a default format. resolution = resolutions[0] if resolution: print("Taking multiple images using the %s format" % resolution['pixelformat']) for res in resolution['resolutions']: w = res[0] h = res[1] f = NamedTemporaryFile(prefix='camera_test_%s%sx%s' % (resolution['pixelformat'], w, h), suffix='.jpg', delete=False) print("Taking a picture at %sx%s" % (w, h)) self._still_helper(f.name, w, h, True, pixelformat=resolution['pixelformat']) if self._validate_image(f.name, w, h): print("Validated image %s" % f.name) os.remove(f.name) else: print("Failed to validate image %s" % f.name, file=sys.stderr) os.remove(f.name) return 1 return 0 def _get_pixel_formats(self, device, maxformats=5): """ Query the camera to see what pixel formats it supports. A list of dicts is returned consisting of format and description. The caller should check whether this camera supports VIDEO_CAPTURE before calling this function. """ supported_formats = [] fmt = v4l2_fmtdesc() fmt.index = 0 fmt.type = V4L2_CAP_VIDEO_CAPTURE try: while fmt.index < maxformats: with open(device, 'r') as vd: if fcntl.ioctl(vd, VIDIOC_ENUM_FMT, fmt) == 0: pixelformat = {} # save the int type for re-use later pixelformat['pixelformat_int'] = fmt.pixelformat pixelformat['pixelformat'] = "%s%s%s%s" % \ (chr(fmt.pixelformat & 0xFF), chr((fmt.pixelformat >> 8) & 0xFF), chr((fmt.pixelformat >> 16) & 0xFF), chr((fmt.pixelformat >> 24) & 0xFF)) pixelformat['description'] = fmt.description.decode() supported_formats.append(pixelformat) fmt.index = fmt.index + 1 except IOError as e: # EINVAL is the ioctl's way of telling us that there are no # more formats, so we ignore it if e.errno != errno.EINVAL: print("Unable to determine Pixel Formats, this may be a " "driver issue.") return supported_formats return supported_formats def _get_supported_resolutions(self, device): """ Query the camera for supported resolutions for a given pixel_format. Data is returned in a list of dictionaries with supported pixel formats as the following example shows: resolution['pixelformat'] = "YUYV" resolution['description'] = "(YUV 4:2:2 (YUYV))" resolution['resolutions'] = [[width, height], [640, 480], [1280, 720] ] If we are unable to gather any information from the driver, then we return YUYV and 640x480 which seems to be a safe default. Per the v4l2 spec the ioctl used here is experimental but seems to be well supported. """ supported_formats = self._get_pixel_formats(device) if not supported_formats: resolution = {} resolution['description'] = "YUYV" resolution['pixelformat'] = "YUYV" resolution['resolutions'] = [[640, 480]] supported_formats.append(resolution) return supported_formats for supported_format in supported_formats: resolutions = [] framesize = v4l2_frmsizeenum() framesize.index = 0 framesize.pixel_format = supported_format['pixelformat_int'] with open(device, 'r') as vd: try: while fcntl.ioctl(vd, VIDIOC_ENUM_FRAMESIZES, framesize) == 0: if framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE: resolutions.append([framesize.discrete.width, framesize.discrete.height]) # for continuous and stepwise, let's just use min and # max they use the same structure and only return # one result elif framesize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS or\ framesize.type == V4L2_FRMSIZE_TYPE_STEPWISE: resolutions.append([framesize.stepwise.min_width, framesize.stepwise.min_height] ) resolutions.append([framesize.stepwise.max_width, framesize.stepwise.max_height] ) break framesize.index = framesize.index + 1 except IOError as e: # EINVAL is the ioctl's way of telling us that there are no # more formats, so we ignore it if e.errno != errno.EINVAL: print("Unable to determine supported framesizes " "(resolutions), this may be a driver issue.") return supported_formats supported_format['resolutions'] = resolutions return supported_formats def _validate_image(self, filename, width, height): """ Given a filename, ensure that the image is the width and height specified and is a valid image file. """ if imghdr.what(filename) != 'jpeg': return False outw = outh = 0 with open(filename, mode='rb') as jpeg: jpeg.seek(2) b = jpeg.read(1) try: while (b and ord(b) != 0xDA): while (ord(b) != 0xFF): b = jpeg.read(1) while (ord(b) == 0xFF): b = jpeg.read(1) if (ord(b) >= 0xC0 and ord(b) <= 0xC3): jpeg.seek(3, 1) h, w = struct.unpack(">HH", jpeg.read(4)) break b = jpeg.read(1) outw, outh = int(w), int(h) except (struct.error, ValueError): pass if outw != width: print("Image width does not match, was %s should be %s" % (outw, width), file=sys.stderr) return False if outh != height: print("Image width does not match, was %s should be %s" % (outh, height), file=sys.stderr) return False return True return True def parse_arguments(argv): """ Parse command line arguments """ parser = argparse.ArgumentParser(description="Run a camera-related test") subparsers = parser.add_subparsers(dest='test', title='test', description='Available camera tests') def add_device_parameter(parser): group = parser.add_mutually_exclusive_group() group.add_argument("-d", "--device", default="/dev/video0", help="Device for the webcam to use") group.add_argument("--highest-device", action="store_true", help=("Use the /dev/videoN " "where N is the highest value available")) group.add_argument("--lowest-device", action="store_true", help=("Use the /dev/videoN " "where N is the lowest value available")) subparsers.add_parser('detect') led_parser = subparsers.add_parser('led') add_device_parameter(led_parser) display_parser = subparsers.add_parser('display') add_device_parameter(display_parser) still_parser = subparsers.add_parser('still') add_device_parameter(still_parser) still_parser.add_argument("-f", "--filename", help="Filename to store the picture") still_parser.add_argument("-q", "--quiet", action="store_true", help=("Don't display picture, " "just write the picture to a file")) resolutions_parser = subparsers.add_parser('resolutions') add_device_parameter(resolutions_parser) args = parser.parse_args(argv) def get_video_devices(): devices = sorted(glob('/dev/video[0-9]'), key=lambda d: re.search(r'\d', d).group(0)) assert len(devices) > 0, "No video devices found" return devices if hasattr(args, 'highest_device') and args.highest_device: args.device = get_video_devices()[-1] elif hasattr(args, 'lowest_device') and args.lowest_device: args.device = get_video_devices()[0] return args if __name__ == "__main__": args = parse_arguments(sys.argv[1:]) if not args.test: args.test = 'detect' # Import Gst only for the test cases that will need it if args.test in ['display', 'still', 'led', 'resolutions']: from gi.repository import Gst if Gst.version()[0] > 0: gst_plugin = 'videoconvert' gst_video_type = 'video/x-raw' else: gst_plugin = 'ffmpegcolorspace' gst_video_type = 'video/x-raw-yuv' Gst.init(None) camera = CameraTest(args, gst_plugin, gst_video_type) else: camera = CameraTest(args) sys.exit(getattr(camera, args.test)()) 2013.com.canonical.certification.checkbox-0.4/bin/obex_send0000775000175000017500000000536112320541306023471 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import time import dbus import dbus.service import dbus.mainloop.glib from gi.repository import GObject class Agent(dbus.service.Object): def __init__(self, conn=None, obj_path=None): dbus.service.Object.__init__(self, conn, obj_path) @dbus.service.method("org.openobex.Agent", in_signature="o", out_signature="s") def Request(self, path): print("Transfer Request") self.transfer = dbus.Interface(bus.get_object("org.openobex.client", path), "org.openobex.Transfer") properties = self.transfer.GetProperties() for key in list(properties.keys()): print(" %s = %s" % (key, properties[key])) self.start = True return "" @dbus.service.method("org.openobex.Agent", in_signature="ot", out_signature="") def Progress(self, path, transferred): if (self.start): print("Transfer Started") properties = self.transfer.GetProperties() self.transfer_size = properties['Size'] self.start_time = time.time() self.start = False else: speed = transferred / abs((time.time() - self.start_time) * 1000) progress = ("(" + str(transferred) + "/" + str(self.transfer_size) + " bytes) @ " + str(int(speed)) + " kB/s") out = "\rTransfer progress " + progress sys.stdout.write(out) sys.stdout.flush() return @dbus.service.method("org.openobex.Agent", in_signature="o", out_signature="") def Complete(self, path): print("\nTransfer finished") return @dbus.service.method("org.openobex.Agent", in_signature="os", out_signature="") def Error(self, path, error): print("\nTransfer finished with an error: %s" % (error)) return @dbus.service.method("org.openobex.Agent", in_signature="", out_signature="") def Release(self): mainloop.quit() return if __name__ == '__main__': dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SessionBus() client = dbus.Interface(bus.get_object("org.openobex.client", "/"), "org.openobex.Client") if (len(sys.argv) < 3): print("Usage: %s [file*]" % (sys.argv[0])) sys.exit(1) path = "/test/agent" agent = Agent(bus, path) mainloop = GObject.MainLoop() files = [os.path.realpath(f) for f in sys.argv[2:]] client.SendFiles({"Destination": sys.argv[1]}, files, path) mainloop.run() 2013.com.canonical.certification.checkbox-0.4/bin/network_wait0000775000175000017500000000041712320541306024235 0ustar zygazyga00000000000000#!/bin/bash set -e x=1 while true; do state=$(/usr/bin/nmcli -t -f STATE nm) if [ "$state" = "connected" ]; then echo $state exit 0 fi x=$(($x + 1)) if [ $x -gt 12 ]; then echo $state exit 1 fi sleep 5 done 2013.com.canonical.certification.checkbox-0.4/bin/process_wait0000775000175000017500000000352412320541306024224 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import time from optparse import OptionParser from subprocess import Popen, PIPE COMMAND_FORMAT = "pgrep -f %(options)s %(process)s" def process_pids(process, *options): options_string = " ".join(options) command = COMMAND_FORMAT % {"options": options_string, "process": process} # Exclude this process and the pgrep process subprocess = Popen( command, stdout=PIPE, shell=True, universal_newlines=True) exclude_pids = [os.getpid(), os.getppid(), subprocess.pid] pids_string = subprocess.communicate()[0] pids = [int(pid) for pid in pids_string.split()] result = set(pids).difference(exclude_pids) return list(result) def process_count(*args): return len(process_pids(*args)) def main(args): default_sleep = 1 usage = "Usage: %prog PROCESS [PROCESS...]" parser = OptionParser(usage=usage) parser.add_option("-s", "--sleep", type="int", default=default_sleep, help="Number of seconds to sleep between checks.") parser.add_option("-t", "--timeout", type="int", help="Number of seconds to timeout from sleeping.") parser.add_option("-u", "--uid", help="Effective user name or id of the running processes") (options, processes) = parser.parse_args(args) process_args = [] if options.uid is not None: process_args.extend(["-u", options.uid]) while True: for process in processes: if process_count(process, *process_args): break else: break if options.timeout is not None: if options.timeout <= 0: return 1 else: options.timeout -= options.sleep time.sleep(options.sleep) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/disk_stats_test0000775000175000017500000000365712320541306024740 0ustar zygazyga00000000000000#!/bin/bash #Simple script to gather some data about a disk to verify it's seen by the OS #and is properly represented. Defaults to sda if not passed a disk at run time DISK="sda" check_return_code() { if [ "${1}" -ne "0" ]; then echo "ERROR: retval ${1} : ${2}" >&2 exit ${1} fi } if [[ "$1" != '' ]]; then DISK="$1" fi #Get some baseline stats for use later echo "Getting baseline stats" PROC_STAT_BEGIN=`grep -m 1 $DISK /proc/diskstats` SYS_STAT_BEGIN=`cat /sys/block/$DISK/stat` #Generate some disk activity using hdparm -t echo "Generating some disk activity" hdparm -t "/dev/$DISK" 2&> /dev/null #Sleep 5 to let the stats files catch up sleep 5 #Check /proc/partitions, exit with fail if disk isn't found echo "Checking /proc/partitions" grep -q $DISK /proc/partitions check_return_code $? "Disk $DISK not found in /proc/partitions" #Next, check /proc/diskstats echo "Checking /proc/diskstats" grep -q -m 1 $DISK /proc/diskstats check_return_code $? "Disk $DISK not found in /proc/diskstats" #Verify the disk shows up in /sys/block/ echo "Checking /sys/block" ls /sys/block | grep -q $DISK check_return_code $? "Disk $DISK not found in /sys/block" #Verify there are stats in /sys/block/$DISK/stat echo "Checking /sys/block/$DISK/stat" [[ -s "/sys/block/$DISK/stat" ]] check_return_code $? "stat is either empty or nonexistant in /sys/block/$DISK/" #Sleep 5 to let the stats files catch up sleep 5 #Make sure the stats have changed: echo "Getting ending stats" PROC_STAT_END=`grep -m 1 $DISK /proc/diskstats` SYS_STAT_END=`cat /sys/block/$DISK/stat` echo "Checking /proc/diskstats for changes" [[ "$PROC_STAT_BEGIN" != "$PROC_STAT_END" ]] check_return_code $? "Stats in /proc/diskstats did not change" echo "Checking /sys/block/$DISK/stat for changes" [[ "$SYS_STAT_BEGIN" != "$SYS_STAT_END" ]] check_return_code $? "Stats in /sys/block/$DISK/stat did not change" echo "PASS: Finished testing stats for $DISK" exit 0 2013.com.canonical.certification.checkbox-0.4/bin/network_device_info0000775000175000017500000002122612320541306025544 0ustar zygazyga00000000000000#!/usr/bin/env python3 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Parts of this are based on the example python code that ships with # NetworkManager # http://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python # # Copyright (C) 2012 Canonical, Ltd. from subprocess import check_output, CalledProcessError, STDOUT import sys import dbus from checkbox_support.parsers.modinfo import ModinfoParser from checkbox_support.parsers.udevadm import UdevadmParser # This example lists basic information about network interfaces known to NM devtypes = {1: "Ethernet", 2: "WiFi", 5: "Bluetooth", 6: "OLPC", 7: "WiMAX", 8: "Modem"} states = {0: "Unknown", 10: "Unmanaged", 20: "Unavailable", 30: "Disconnected", 40: "Prepare", 50: "Config", 60: "Need Auth", 70: "IP Config", 80: "IP Check", 90: "Secondaries", 100: "Activated", 110: "Deactivating", 120: "Failed"} attributes = ("category", "interface", "product", "vendor", "driver", "path") udev_devices = [] nm_devices = [] class UdevResult: def addDevice(self, device): if device.interface: udev_devices.append(device) class NetworkingDevice(): def __init__(self, devtype, props, dev_proxy, bus): self._devtype = devtype try: self._interface = props['Interface'] except KeyError: self._interface = "Unknown" try: self._ip = self._int_to_ip(props['Ip4Address']) except KeyError: self._ip = "Unknown" try: self._driver = props['Driver'] except KeyError: self._driver = "Unknown" self._driver_ver = "Unknown" if self._driver != "Unknown": self._modinfo = self._modinfo_parser(props['Driver']) if self._modinfo: self._driver_ver = self._find_driver_ver() else: self._driver_ver = "Unknown" try: self._firmware_missing = props['FirmwareMissing'] except KeyError: self._firmware_missing = False try: self._state = states[props['State']] except KeyError: self._state = "Unknown" def __str__(self): ret = "Category: %s\n" % self._devtype ret += "Interface: %s\n" % self._interface ret += "IP: %s\n" % self._ip ret += "Driver: %s (ver: %s)\n" % (self._driver, self._driver_ver) if self._firmware_missing: ret += "Warning: Required Firmware Missing for device\n" ret += "State: %s\n" % self._state return ret def getstate(self): return self._state def gettype(self): return self._devtype def _bitrate_to_mbps(self, bitrate): try: intbr = int(bitrate) return str(intbr / 1000) except Exception: return "NaN" def _modinfo_parser(self, driver): cmd = ['/sbin/modinfo', driver] try: stream = check_output(cmd, stderr=STDOUT, universal_newlines=True) except CalledProcessError as err: print("Error running %s:" % ' '.join(cmd), file=sys.stderr) print(err.output, file=sys.stderr) return None if not stream: print("Error: modinfo returned nothing", file=sys.stderr) return None else: parser = ModinfoParser(stream) modinfo = parser.get_all() return modinfo def _find_driver_ver(self): # try the version field first, then vermagic second, some audio # drivers don't report version if the driver is in-tree if self._modinfo['version'] and self._modinfo['version'] != 'in-tree:': return self._modinfo['version'] else: # vermagic will look like this (below) and we only care about the # first part: # "3.2.0-29-generic SMP mod_unload modversions" return self._modinfo['vermagic'].split()[0] def _int_to_ip(self, int_ip): ip = [0, 0, 0, 0] ip[0] = int_ip & 0xff ip[1] = (int_ip >> 8) & 0xff ip[2] = (int_ip >> 16) & 0xff ip[3] = (int_ip >> 24) & 0xff return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3]) def get_nm_devices(): devices = [] bus = dbus.SystemBus() # Get a proxy for the base NetworkManager object proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") manager = dbus.Interface(proxy, "org.freedesktop.NetworkManager") # Get all devices known to NM and print their properties nm_devices = manager.GetDevices() for d in nm_devices: dev_proxy = bus.get_object("org.freedesktop.NetworkManager", d) prop_iface = dbus.Interface(dev_proxy, "org.freedesktop.DBus.Properties") props = prop_iface.GetAll("org.freedesktop.NetworkManager.Device") try: devtype = devtypes[props['DeviceType']] except KeyError: devtype = "Unknown" # only return WiFi, Ethernet and Modem devices if devtype in ("WiFi", "Ethernet", "Modem"): devices.append(NetworkingDevice(devtype, props, dev_proxy, bus)) return devices def match_counts(nm_devices, udev_devices, devtype): """ Ensures that the count of devices matching devtype is the same for the two passed in lists, devices from Network Manager and devices from lspci. """ # now check that the count (by type) matches nm_type_devices = [dev for dev in nm_devices if dev.gettype() in devtype] udevtype = 'WIRELESS' if devtype == 'WiFi' else 'NETWORK' udev_type_devices = [ udev for udev in udev_devices if udev.category == udevtype] if len(nm_type_devices) != len(udev_type_devices): print("ERROR: devices missing - udev showed %d %s devices, but " "NetworkManager saw %d devices in %s" % (len(udev_type_devices), udevtype, len(nm_type_devices), devtype), file=sys.stderr) return False else: return True def main(args): try: output = check_output(['udevadm', 'info', '--export-db']) except CalledProcessError as err: raise SystemExit(err) try: output = output.decode("UTF-8", errors='ignore') except UnicodeDecodeError as err: raise SystemExit("udevadm output is not valid UTF-8") udev = UdevadmParser(output) result = UdevResult() udev.run(result) if udev_devices: print("[ Devices found by udev ]".center(80, '-')) for device in udev_devices: for attribute in attributes: value = getattr(device, attribute) if value is not None: if attribute == 'driver': props = {} props['Driver'] = value network_dev = NetworkingDevice(None, props, None, None) print("%s: %s (ver: %s)" % (attribute.capitalize(), value, network_dev._driver_ver)) else: print("%s: %s" % (attribute.capitalize(), value)) print() try: nm_devices = get_nm_devices() except dbus.exceptions.DBusException as e: # server's don't have network manager installed print("Warning: Exception while talking to Network Manager over dbus." " Skipping the remainder of this test. If this is a server, this" " is expected.", file=sys.stderr) print("The Error Generated was:\n %s" % e, file=sys.stderr) return 0 print("[ Devices found by Network Manager ]".center(80, '-')) for nm_dev in nm_devices: print(nm_dev) if not match_counts(nm_devices, udev_devices, "WiFi"): return 1 elif not match_counts(nm_devices, udev_devices, ("Ethernet","Modem")): return 1 else: return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/tomcat_test0000775000175000017500000000104212320541306024041 0ustar zygazyga00000000000000#!/bin/bash # # Confirm Tomcat server is running and working properly # Requires: tomcat6 # # Check tomcat is running run1=`netstat -ltnp | grep '8080' | grep 'java'` if [ -z "$run1" ]; then echo "FAIL: Tomcat is not running." exit 1 fi # Check if Tomcat is working; requires network connection so check it check=`ping -c 2 www.ubuntu.com |grep "2 received"` if [ -n "$check" ]; then work1=`w3m http://127.0.0.1:8080 | grep "works"` if [ -z "$work1" ]; then echo "FAIL: Tomcat is not working properly." exit 1 fi fi exit 0 2013.com.canonical.certification.checkbox-0.4/bin/sleep_test0000775000175000017500000003476612320541306023705 0ustar zygazyga00000000000000#!/usr/bin/python ''' Program to automate system entering and resuming from sleep states Copyright (C) 2010,2011 Canonical Ltd. Author: Jeff Lane This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import os import sys import logging import re from subprocess import call from optparse import OptionParser, OptionGroup from syslog import * class ListDictHandler(logging.StreamHandler): ''' Extends logging.StreamHandler to handle list, tuple and dict objects internally, rather than through external code, mainly used for debugging purposes. ''' def emit(self, record): if isinstance(record.msg, (list, tuple,)): for msg in record.msg: logger = logging.getLogger(record.name) new_record = logger.makeRecord(record.name, record.levelno, record.pathname, record.lineno, msg, record.args, record.exc_info, record.funcName) logging.StreamHandler.emit(self, new_record) elif isinstance(record.msg, dict): for key, val in record.msg.iteritems(): logger = logging.getLogger(record.name) new_msg = '%s: %s' % (key, val) new_record = logger.makeRecord(record.name, record.levelno, record.pathname, record.lineno, new_msg, record.args, record.exc_info, record.funcName) logging.StreamHandler.emit(self, new_record) else: logging.StreamHandler.emit(self, record) class SuspendTest(): ''' Creates an object to handle the actions necessary for suspend/resume testing. ''' def __init__(self): self.wake_time = 0 self.current_time = 0 self.last_time = 0 def CanWeSleep(self, mode): ''' Test to see if S3 state is available to us. /proc/acpi/* is old and will be deprecated, using /sys/power to maintain usefulness for future kernels. ''' states_fh = open('/sys/power/state', 'r', 0) try: states = states_fh.read().split() finally: states_fh.close() logging.debug('The following sleep states were found:') logging.debug(states) if mode in states: return True else: return False def GetCurrentTime(self): time_fh = open('/sys/class/rtc/rtc0/since_epoch', 'r', 0) try: time = int(time_fh.read()) finally: time_fh.close() return time def SetWakeTime(self, time): ''' Get the current epoch time from /sys/class/rtc/rtc0/since_epoch then add time and write our new wake_alarm time to /sys/class/rtc/rtc0/wakealarm. The math could probably be done better but this method avoids having to worry about whether or not we're using UTC or local time for both the hardware and system clocks. ''' self.last_time = self.GetCurrentTime() logging.debug('Current epoch time: %s' % self.last_time) wakealarm_fh = open('/sys/class/rtc/rtc0/wakealarm', 'w', 0) try: wakealarm_fh.write('0\n') wakealarm_fh.flush() wakealarm_fh.write('+%s\n' % time) wakealarm_fh.flush() finally: wakealarm_fh.close() logging.debug('Wake alarm in %s seconds' % time) def DoSuspend(self, mode): ''' Suspend the system and hope it wakes up. Previously tried writing new state to /sys/power/state but that seems to put the system into an uncrecoverable S3 state. So far, pm-suspend seems to be the most reliable way to go. ''' # Set up our start and finish markers self.time_stamp = self.GetCurrentTime() self.start_marker = 'CHECKBOX SLEEP TEST START %s' % self.time_stamp self.end_marker = 'CHECKBOX SLEEP TEST STOP %s' % self.time_stamp self.MarkLog(self.start_marker) if mode == 'mem': status = call('/usr/sbin/pm-suspend') elif mode == 'disk': status = call('/usr/sbin/pm-hibernate') else: logging.debug('Unknown sleep state passed: %s' % mode) status == 1 if status == 0: logging.debug('Successful suspend') else: logging.debug('Error while running pm-suspend') self.MarkLog(self.end_marker) def GetResults(self, mode, perf): ''' This will parse /var/log/messages for our start and end markers. Then it'll find a few key phrases that are part of the sleep and resume process, grab their timestamps, Bob's your Uncle and return a three-tuple consisting of: (PASS/FAIL,Sleep elapsed time, Resume elapsed time) ''' # figure out our elapsed time logfile = '/var/log/syslog' log_fh = open(logfile, 'r') line = '' run_complete = 'Fail' sleep_start_time = 0.0 sleep_end_time = 0.0 resume_start_time = 0.0 resume_end_time = 0.0 while self.start_marker not in line: line = log_fh.readline() if self.start_marker in line: logging.debug("Found Start Marker") loglist = log_fh.readlines() if perf: for idx in range(0, len(loglist)): if 'PM: Syncing filesystems' in loglist[idx]: sleep_start_time = re.split('[\[\]]', loglist[idx])[1].strip() logging.debug('Sleep started at %s' % sleep_start_time) if 'ACPI: Low-level resume complete' in loglist[idx]: sleep_end_time = re.split('[\[\]]', loglist[idx - 1])[1].strip() logging.debug('Sleep ended at %s' % sleep_end_time) resume_start_time = re.split('[\[\]]', loglist[idx])[1].strip() logging.debug('Resume started at %s' % resume_start_time) idx += 1 if 'Restarting tasks' in loglist[idx]: resume_end_time = re.split('[\[\]]', loglist[idx])[1].strip() logging.debug('Resume ended at %s' % resume_end_time) if self.end_marker in loglist[idx]: logging.debug('End Marker found, run appears to ' 'have completed') run_complete = 'Pass' break sleep_elapsed = float(sleep_end_time) - float(sleep_start_time) resume_elapsed = float(resume_end_time) - float(resume_start_time) logging.debug('Sleep elapsed: %.4f seconds' % sleep_elapsed) logging.debug('Resume elapsed: %.4f seconds' % resume_elapsed) else: if self.end_marker in loglist: logging.debug('End Marker found, ' 'run appears to have completed') run_complete = 'Pass' sleep_elapsed = None resume_elapsed = None return (run_complete, sleep_elapsed, resume_elapsed) def MarkLog(self, marker): ''' Write a stamped marker to syslogd (will appear in /var/log/messages). This is used to calculate the elapsed time for each iteration. ''' syslog(LOG_INFO, '---' + marker + '---') def CheckAlarm(self, mode): ''' A better method for checking if system woke via rtc alarm IRQ. If the system woke via IRQ, then alarm_IRQ will be 'no' and wakealarm will be an empty file. Otherwise, alarm_IRQ should still say yes and wakealarm should still have a number in it (the original alarm time), indicating the system did not wake by alarm IRQ, but by some other means. ''' rtc = {} rtc_fh = open('/proc/driver/rtc', 'r', 0) alarm_fh = open('/sys/class/rtc/rtc0/wakealarm', 'r', 0) try: rtc_data = rtc_fh.read().splitlines() for item in rtc_data: rtc_entry = item.partition(':') rtc[rtc_entry[0].strip()] = rtc_entry[2].strip() finally: rtc_fh.close() try: alarm = int(alarm_fh.read()) except ValueError: alarm = None finally: alarm_fh.close() logging.debug('Current RTC entries') logging.debug(rtc) logging.debug('Current wakealarm %s' % alarm) # see if there's something in wakealarm or alarm_IRQ # Return True indicating the alarm is still set # Return False indicating the alarm is NOT set. # This is currently held up by a bug in PM scripts that # does not reset alarm_IRQ when waking from hibernate. # https://bugs.launchpad.net/ubuntu/+source/linux/+bug/571977 if mode == 'mem': if (alarm is not None) or (rtc['alarm_IRQ'] == 'yes'): logging.debug('alarm is %s' % alarm) logging.debug('rtc says alarm_IRQ: %s' % rtc['alarm_IRQ']) return True else: logging.debug('alarm was cleared') return False else: # This needs to be changed after we get a way around the # hibernate bug. For now, pretend that the alarm is unset for # hibernate tests. logging.debug('mode is %s so we\'re skipping success check' % mode) return False def main(): usage = 'Usage: %prog [OPTIONS]' parser = OptionParser(usage) group = OptionGroup(parser, 'This will not work for hibernat testing due' \ ' to a kernel timestamp bug when doing an S4 ' \ '(hibernate/resume) sleep cycle') group.add_option('-p', '--perf', action='store_true', default=False, help='Add some output that tells you how long it ' \ 'takes to enter a sleep state and how long it ' \ 'takes to resume.') parser.add_option('-i', '--iterations', action='store', type='int', metavar='NUM', default=1, help='The number of times to run the suspend/resume \ loop. Default is %default') parser.add_option('-w', '--wake-in', action='store', type='int', metavar='NUM', default=60, dest='wake_time', help='Sets wake up time (in seconds) in the future \ from now. Default is %default.') parser.add_option('-s', '--sleep-state', action='store', default='mem', metavar='MODE', dest='mode', help='Sets the sleep state to test. Passing mem will \ set the sleep state to Suspend-To-Ram or S3. Passing \ disk will set the sleep state to Suspend-To-Disk or S4\ (hibernate). Default sleep state is %default') parser.add_option('-d', '--debug', action='store_true', default=False, help='Choose this to add verbose output for debug \ purposes') parser.add_option_group(group) (options, args) = parser.parse_args() options_dict = vars(options) if not (os.geteuid() == 0): parser.error("Must be run as root.") return 1 # Set up logging handler format = '%(message)s' handler = ListDictHandler(sys.stdout) handler.setFormatter(logging.Formatter(format)) handler.setLevel(logging.INFO) # Set up the logger logger = logging.getLogger() logger.setLevel(logging.DEBUG) if options.debug: handler.setLevel(logging.DEBUG) logger.addHandler(handler) logging.debug('Running with these options') logging.debug(options_dict) suspender = SuspendTest() run_result = {} run_count = 0 fail_count = 0 # Chcek fo the S3 state availability if not suspender.CanWeSleep(options.mode): logging.error('%s sleep state not supported' % options.mode) return 1 else: logging.debug('%s sleep state supported, continuing test' % options.mode) # We run the following for the number of iterations requested for iteration in range(0, options.iterations): # Set new alarm time and suspend. suspender.SetWakeTime(options.wake_time) suspender.DoSuspend(options.mode) run_count += 1 run_result[run_count] = suspender.GetResults(options.mode, options.perf) if suspender.CheckAlarm(options.mode): logging.debug('The alarm is still set') if options.perf: sleep_total = 0.0 resume_total = 0.0 logging.info('=' * 20 + ' Test Results ' + '=' * 20) logging.info(run_result) for k in run_result.iterkeys(): sleep_total += run_result[k][1] resume_total += run_result[k][2] sleep_avg = sleep_total / run_count resume_avg = resume_total / run_count logging.info('Average time to sleep: %.4f' % sleep_avg) logging.info('Average time to resume: %.4f' % resume_avg) for run in run_result.keys(): if 'Fail' in run_result[run]: fail_count += 1 if fail_count > 0: logging.error('%s sleep/resume test cycles failed' % fail_count) logging.error(run_result) return 1 else: logging.info('Successfully completed %s sleep iterations' % options.iterations) return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network0000775000175000017500000005046712320541306023223 0ustar zygazyga00000000000000#!/usr/bin/env python3 """ Copyright (C) 2012-2014 Canonical Ltd. Authors Jeff Marcom Daniel Manrique This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ from argparse import ( ArgumentParser, RawTextHelpFormatter ) import configparser import fcntl import ftplib from ftplib import FTP import logging import os import re import shlex import socket import struct import subprocess from subprocess import ( CalledProcessError, check_call, check_output ) import sys import time logging.basicConfig(level=logging.DEBUG) class IPerfPerformanceTest(object): """Measures performance of interface using iperf client and target. Calculated speed is measured against theorectical throughput of selected interface""" def __init__( self, interface, target, fail_threshold, protocol="tcp", mbytes="1024M"): self.iface = Interface(interface) self.target = target self.protocol = protocol self.fail_threshold = fail_threshold self.mbytes = mbytes def run(self): cmd = "timeout 180 iperf -c {} -n {}".format(self.target, self.mbytes) logging.debug(cmd) try: iperf_return = check_output( shlex.split(cmd), universal_newlines=True) except CalledProcessError as iperf_exception: if iperf_exception.returncode != 124: # timeout command will return 124 if iperf timed out, so any # other return value means something did fail logging.error("Failed executing iperf: %s", iperf_exception.output) return iperf_exception.returncode else: # this is normal so we "except" this exception and we # "pass through" whatever output iperf did manage to produce. # When confronted with SIGTERM iperf should stop and output # a partial (but usable) result. logging.warning("iperf timed out - this should be OK") iperf_return = iperf_exception.output # 930 Mbits/sec\n' print(iperf_return) match = re.search(r'[\d\.]+\s([GM])bits', iperf_return) if match: throughput = match.group(0).split()[0] units = match.group(1) # self.iface.max_speed is always in mb/s, so we need to scale # throughput to match scaled_throughput = float(throughput) if units == 'G': scaled_throughput *= 1000 if units == 'K': scaled_throughput /= 1000 try: percent = scaled_throughput / int(self.iface.max_speed) * 100 except (ZeroDivisionError, TypeError) as error: # Catches a condition where the interface functions fine but # ethtool fails to properly report max speed. In this case # it's up to the reviewer to pass or fail. logging.error("Max Speed was not reported properly. Run " "ethtool and verify that the card is properly " "reporting its capabilities.") logging.error(error) percent = 0 print("\nTransfer speed: {} {}b/s".format(throughput, units)) print("%3.2f%% of " % percent, end="") try: print("theoretical max %sMb/s\n" % int(self.iface.max_speed)) except TypeError as error: logging.error("Max Speed was not reported properly. Run " "ethtool and verify that the card is properly " "reporting its capabilities.") logging.error(error) if percent < self.fail_threshold: logging.warn("Poor network performance detected") return 30 logging.debug("Passed benchmark") else: print("Failed iperf benchmark") return 1 class FTPPerformanceTest(object): """Provides file transfer rate based information while using the FTP protocol and sending a file (DEFAULT=1GB) over the local or public network using a specified network interface on the host.""" def __init__( self, target, username, password, interface, binary_size=1, file2send="ftp_performance_test"): self.target = target self.username = username self.password = password self.iface = Interface(interface) self.binary_size = binary_size self.file2send = file2send def _make_file2send(self): """ Makes binary file to send over FTP. Size defaults to 1GB if not supplied. """ logging.debug("Creating %sGB file", self.binary_size) file_size = (1024 * 1024 * 1024) * self.binary_size with open(self.file2send, "wb") as out: out.seek((file_size) - 1) out.write('\0'.encode()) def send_file(self, filename=None): """ Sends file over the network using FTP and returns the amount of bytes sent and delay between send and completed. """ if filename is None: file = open(self.file2send, 'rb') filename = self.file2send send_time = time.time() try: logging.debug("Sending file") self.remote.storbinary("STOR " + filename, file, 1024) except (ftplib.all_errors) as send_failure: logging.error("Failed to send file to %s", self.target) logging.error("Reason: %s", send_failure) return 0, 0 file.close() time_lapse = time.time() - send_time bytes_sent = os.stat(filename).st_size return bytes_sent, time_lapse def close_connection(self): """ Close connection to remote FTP target """ self.remote.close() def connect(self): """ Connects to FTP target and set the current directory as / """ logging.debug("Connecting to %s", self.target) try: self.remote = FTP(self.target) self.remote.set_debuglevel(2) self.remote.set_pasv(True) except socket.error as connect_exception: logging.error("Failed to connect to: %s", self.target) return False logging.debug("Logging in") logging.debug("{USER:%s, PASS:%s}", self.username, self.password) try: self.remote.login(self.username, self.password) except ftplib.error_perm as login_exception: logging.error("failed to log into target: %s", self.target) return False default_out_dir = "" self.remote.cwd(default_out_dir) return True def run(self): info = { "Interface": self.iface.interface, "HWAddress": self.iface.macaddress, "Duplex": self.iface.duplex_mode, "Speed": self.iface.max_speed, "Status": self.iface.status } logging.debug(info) if not os.path.isfile(self.file2send): self._make_file2send() # Connect to FTP target and send file connected = self.connect() if connected is False: return 3 filesize, delay = self.send_file() # Remove created binary try: os.remove(self.file2send) except (IOError, OSError) as file_delete_error: logging.error("Could not remove previous ftp file") logging.error(file_delete_error) if connected and filesize > 0: logging.debug("Bytes sent (%s): %.2f seconds", filesize, delay) # Calculate transfer rate and determine pass/fail status mbs_speed = float(filesize / 131072) / float(delay) percent = (mbs_speed / int(info["Speed"])) * 100 print("Transfer speed:") print("%3.2f%% of" % percent) print("theoretical max %smbs" % int(info["Speed"])) if percent < 40: logging.warn("Poor network performance detected") return 30 logging.debug("Passed benchmark") else: print("Failed sending file via ftp") return 1 class StressPerformanceTest: def __init__(self, interface, target): self.interface = interface self.target = target def run(self): iperf_cmd = 'timeout 320 iperf -c {} -t 300'.format(self.target) print("Running iperf...") iperf = subprocess.Popen(shlex.split(iperf_cmd)) ping_cmd = 'ping -I {} {}'.format(self.interface, self.target) ping = subprocess.Popen(shlex.split(ping_cmd), stdout=subprocess.PIPE) iperf.communicate() ping.terminate() (out, err) = ping.communicate() if iperf.returncode != 0: return iperf.returncode print("Running ping test...") result = 0 time_re = re.compile('(?<=time=)[0-9]*') for line in out.decode().split('\n'): time = time_re.search(line) if time and int(time.group()) > 2000: print(line) print("ICMP packet was delayed by > 2000 ms.") result = 1 if 'unreachable' in line.lower(): print(line) result = 1 return result class Interface(socket.socket): """ Simple class that provides network interface information. """ def __init__(self, interface): super(Interface, self).__init__( socket.AF_INET, socket.IPPROTO_ICMP) self.interface = interface self.dev_path = os.path.join("/sys/class/net", self.interface) def _read_data(self, type): try: return open(os.path.join(self.dev_path, type)).read().strip() except OSError: print("{}: Attribute not found".format(type)) @property def ipaddress(self): freq = struct.pack('256s', self.interface[:15].encode()) try: nic_data = fcntl.ioctl(self.fileno(), 0x8915, freq) except IOError: logging.error("No IP address for %s", self.interface) return 1 return socket.inet_ntoa(nic_data[20:24]) @property def netmask(self): freq = struct.pack('256s', self.interface.encode()) try: mask_data = fcntl.ioctl(self.fileno(), 0x891b, freq) except IOError: logging.error("No netmask for %s", self.interface) return 1 return socket.inet_ntoa(mask_data[20:24]) @property def max_speed(self): return self._read_data("speed") @property def macaddress(self): return self._read_data("address") @property def duplex_mode(self): return self._read_data("duplex") @property def status(self): return self._read_data("operstate") @property def device_name(self): return self._read_data("device/label") def get_test_parameters(args, environ, config_filename): # Decide the actual values for test parameters, which can come # from one of three possible sources: a config file, command-line # arguments, or environment variables. # - If command-line args were given, they take precedence # - Next come environment variables, if set. # - Last, values in the config file are used if present. params = {"test_target_ftp": None, "test_user": None, "test_pass": None, "test_target_iperf": None} #First (try to) load values from config file config = configparser.SafeConfigParser() try: with open(config_filename) as config_file: config.readfp(config_file) params["test_target_ftp"] = config.get("FTP", "Target") params["test_user"] = config.get("FTP", "User") params["test_pass"] = config.get("FTP", "Pass") params["test_target_iperf"] = config.get("IPERF", "Target") except FileNotFoundError as err: pass # No biggie, we can still get configs from elsewhere # Next see if we have environment variables to override the config file # "partial" overrides are not allowed; if an env variable is missing, # we won't use this at all. if all([param.upper() in os.environ for param in params.keys()]): for key in params.keys(): params[key] = os.environ[key.upper()] # Finally, see if we have the command-line arguments that are the ultimate # override. Again, we will only override if we have all of them. if args.target and args.username and args.password: params["test_target_ftp"] = args.target params["test_user"] = args.username params["test_pass"] = args.password params["test_target_iperf"] = args.target return params def interface_test(args): if not "test_type" in vars(args): return # Determine whether to use the default or user-supplied config # file name. DEFAULT_CFG = "/etc/checkbox.d/network.cfg" if not "config" in vars(args): config_filename = DEFAULT_CFG else: config_filename = args.config # Get the actual test data from one of three possible sources test_parameters = get_test_parameters(args, os.environ, config_filename) test_user = test_parameters["test_user"] test_pass = test_parameters["test_pass"] if (args.test_type.lower() == "iperf" or args.test_type.lower() == "stress"): test_target = test_parameters["test_target_iperf"] else: test_target = test_parameters["test_target_ftp"] # Validate that we got reasonable values if not test_target or "example.com" in test_target: # Default values found in config file logging.error("Please supply target via: %s", config_filename) sys.exit(1) # Testing begins here! # # Check and make sure that interface is indeed connected try: cmd = "ip link set dev %s up" % args.interface check_call(shlex.split(cmd)) except CalledProcessError as interface_failure: logging.error("Failed to use %s:%s", cmd, interface_failure) return 1 # Give interface enough time to get DHCP address time.sleep(10) result = 0 # Stop all other interfaces extra_interfaces = \ [iface for iface in os.listdir("/sys/class/net") if iface != "lo" and iface != args.interface] for iface in extra_interfaces: logging.debug("Shutting down interface:%s", iface) try: cmd = "ip link set dev %s down" % iface check_call(shlex.split(cmd)) except CalledProcessError as interface_failure: logging.error("Failed to use %s:%s", cmd, interface_failure) result = 3 if result == 0: # Execute FTP transfer benchmarking test if args.test_type.lower() == "ftp": ftp_benchmark = FTPPerformanceTest( test_target, test_user, test_pass, args.interface) if args.filesize: ftp_benchmark.binary_size = int(args.filesize) result = ftp_benchmark.run() elif args.test_type.lower() == "iperf": iperf_benchmark = IPerfPerformanceTest(args.interface, test_target, args.fail_threshold) result = iperf_benchmark.run() elif args.test_type.lower() == "stress": stress_benchmark = StressPerformanceTest(args.interface, test_target) result = stress_benchmark.run() for iface in extra_interfaces: logging.debug("Restoring interface:%s", iface) try: cmd = "ip link set dev %s up" % iface check_call(shlex.split(cmd)) except CalledProcessError as interface_failure: logging.error("Failed to use %s:%s", cmd, interface_failure) result = 3 return result def interface_info(args): info_set = "" if "all" in vars(args): info_set = args.all for key, value in vars(args).items(): if value is True or info_set is True: key = key.replace("-", "_") try: print( key + ":", getattr(Interface(args.interface), key), file=sys.stderr) except AttributeError: pass def main(): intro_message = """ Network module This script provides benchmarking and information for a specified network interface. Example NIC information usage: network info -i eth0 --max-speed For running ftp benchmark test: network test -i eth0 -t ftp --target 192.168.0.1 --username USERID --password PASSW0RD --filesize-2 Configuration ============= Configuration can be supplied in three different ways, with the following priorities: 1- Command-line parameters (see above). 2- Environment variables (example will follow). 3- Configuration file (example will follow). Default config location is /etc/checkbox.d/network.cfg Environment variables ===================== ALL environment variables must be defined, even if empty, for them to be picked up. The variables are: TEST_TARGET_FTP TEST_USER TEST_PASS TEST_TARGET_IPERF example config file =================== [FTP] Target: 192.168.1.23 User: FTPUser Pass:PassW0Rd [IPERF] Target: 192.168.1.45 **NOTE** """ parser = ArgumentParser( description=intro_message, formatter_class=RawTextHelpFormatter) subparsers = parser.add_subparsers() # Main cli options test_parser = subparsers.add_parser( 'test', help=("Run network performance test")) info_parser = subparsers.add_parser( 'info', help=("Gather network info")) # Sub test options test_parser.add_argument( '-i', '--interface', type=str, required=True) test_parser.add_argument( '-t', '--test_type', type=str, choices=("ftp", "iperf", "stress"), default="ftp", help=("[FTP *Default*]")) test_parser.add_argument('--target', type=str) test_parser.add_argument( '--username', type=str, help=("For FTP test only")) test_parser.add_argument( '--password', type=str, help=("For FTP test only")) test_parser.add_argument( '--filesize', type=str, help="Size (GB) of binary file to send **Note** for FTP test only") test_parser.add_argument( '--config', type=str, default="/etc/checkbox.d/network.cfg", help="Supply config file for target/host network parameters") test_parser.add_argument( '--fail-threshold', type=int, default=40, help=("IPERF Test ONLY. Set the failure threshold (Percent of maximum " "theoretical bandwidth) as a number like 80. (Default is " "%(default)s)")) # Sub info options info_parser.add_argument( '-i', '--interface', type=str, required=True) info_parser.add_argument( '--all', default=False, action="store_true") info_parser.add_argument( '--duplex-mode', default=False, action="store_true") info_parser.add_argument( '--max-speed', default=False, action="store_true") info_parser.add_argument( '--ipaddress', default=False, action="store_true") info_parser.add_argument( '--netmask', default=False, action="store_true") info_parser.add_argument( '--device-name', default=False, action="store_true") info_parser.add_argument( '--macaddress', default=False, action="store_true") info_parser.add_argument( '--status', default=False, action="store_true", help=("displays connection status")) test_parser.set_defaults(func=interface_test) info_parser.set_defaults(func=interface_info) args = parser.parse_args() return args.func(args) if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network_check0000775000175000017500000000350412320541306024346 0ustar zygazyga00000000000000#!/usr/bin/env python3 """ Check that it's possible to establish a http connection against ubuntu.com """ from subprocess import call from argparse import ArgumentParser import http.client import urllib.request, urllib.error, urllib.parse import sys def check_url(url): """ Open URL and return True if no exceptions were raised """ try: urllib.request.urlopen(url) except (urllib.error.URLError, http.client.InvalidURL): return False return True def main(): """ Check HTTP and connection """ parser = ArgumentParser() parser.add_argument('-u', '--url', action='store', default='http://cdimage.ubuntu.com', help='The target URL to try. Default is %(default)s') parser.add_argument('-a', '--auto', action='store_true', default=False, help='Runs in Automated mode, with no visible output') args = parser.parse_args() url = {"http": args.url} results = {} for protocol, value in url.items(): results[protocol] = check_url(value) bool2str = {True: 'Success', False: 'Failed'} message = ("HTTP connection: %(http)s\n" % dict([(protocol, bool2str[value]) for protocol, value in results.items()])) command = ["zenity", "--title=Network", "--text=%s" % message] if all(results.values()): command.append("--info") else: command.append("--error") if not args.auto: try: call(command) except OSError: print("Zenity missing; unable to report test result:\n %s" % message) if any(results.values()): return 0 else: return 1 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network_restart0000775000175000017500000002454112320541306024761 0ustar zygazyga00000000000000#!/usr/bin/env python3 """ Reboot networking, wait for a while and check if it's possible to send some pings """ import sys import os import time import threading import logging import logging.handlers from subprocess import check_output, check_call, CalledProcessError, STDOUT from argparse import ArgumentParser try: from gi.repository import Gtk, GObject, GLib GLib.threads_init() GObject.threads_init() gtk_found = True except (ImportError, RuntimeError): gtk_found = False class PingError(Exception): def __init__(self, address, reason): self.address = address self.reason = reason def main(): args = parse_args() # Verify that script is run as root if os.getuid(): sys.stderr.write( 'This script needs superuser permissions to run correctly\n') return 1 configure_logging(args.log_level, args.output) # Select interface based on graphich capabilities available if 'DISPLAY' in os.environ and gtk_found: factory = GtkApplication else: factory = CliApplication app = factory(args.address, args.times) return app.run() class Application: """ Network restart application """ def __init__(self, address, times): self.address = address self.times = times self.return_code = 0 def run(self): """ Restart networking as many times as requested and use ping to verify """ networking = Networking(self.address) logging.info('Initial connectivity check') success = ping(self.address) if not success: raise PingError(self.address, 'Some interface is down') for i in range(self.times): if self.return_code: break if self.progress_cb: fraction = float(i) / self.times self.progress_cb(fraction) logging.info('Iteration {0}/{1}...'.format(i + 1, self.times)) networking.restart() else: if self.progress_cb: self.progress_cb(1.0) logging.info('Test successful') return self.return_code class CliApplication(Application): progress_cb = None class GtkApplication(Application): def __init__(self, address, times): Application.__init__(self, address, times) dialog = Gtk.Dialog(title='Network restart', buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) dialog.set_default_size(300, 100) alignment = Gtk.Alignment() alignment.set(0.5, 0.5, 1.0, 0.1) alignment.set_padding(10, 10, 10, 10) progress_bar = Gtk.ProgressBar() progress_bar.set_show_text(True) alignment.add(progress_bar) content_area = dialog.get_content_area() content_area.pack_start(alignment, expand=True, fill=True, padding=0) dialog.connect('response', self.response_cb) dialog.show_all() # Add new logger handler to write info logs to progress bar logger = logging.getLogger() stream = ProgressBarWriter(progress_bar) formatter = logging.Formatter('%(message)s') log_handler = logging.StreamHandler(stream) log_handler.setLevel(logging.INFO) log_handler.setFormatter(formatter) logger.addHandler(log_handler) self.return_code = 0 self.dialog = dialog self.progress_bar = progress_bar self.progress_log_handler = log_handler def response_cb(self, dialog, response_id): """ Cancel test case execution when cancel or close button are closed """ self.return_code = response_id logging.info('Test cancelled') Gtk.main_quit() def progress_cb(self, fraction): """ Update progress bar """ GLib.idle_add(self.progress_bar.set_fraction, fraction) def thread_target(self): """ Run test case in a separate thread """ try: Application.run(self) except PingError as exception: logging.error('Failed to ping {0!r}\n{1}' .format(exception.address, exception.reason)) self.return_code = -1 except CalledProcessError: self.return_code = -1 finally: Gtk.main_quit() def run(self): """ Launch test case and gtk mainloop """ thread = threading.Thread(target=self.thread_target) thread.daemon = True thread.start() Gtk.main() return self.return_code class ProgressBarWriter: """ Write logs to a progress bar """ def __init__(self, progressbar): self.progressbar = progressbar def write(self, message): if message == '\n': return GLib.idle_add(self.progressbar.set_text, message) def ping(address): """ Send ping to a given address """ logging.info('Pinging {0!r}...'.format(address)) try: check_call('ping -c 1 -w 5 {0}'.format(address), stdout=open(os.devnull, 'w'), stderr=STDOUT, shell=True) except CalledProcessError: return False return True class Networking: """ Networking abstraction to start/stop all interfaces """ def __init__(self, address): self.address = address self.interfaces = self._get_interfaces() def _get_interfaces(self): """ Get all network interfaces """ output = check_output(['/sbin/ifconfig', '-s', '-a']) lines = output.splitlines()[1:] interfaces = ([interface for interface in [line.split()[0] for line in lines] if interface != 'lo']) return interfaces def restart(self): """ Restart networking """ self._stop() self._start() def _start(self): """ Start networking """ logging.info('Bringing all interfaces up...') for interface in self.interfaces: try: check_output(['/sbin/ifconfig', interface, 'up']) except CalledProcessError: logging.error('Unable to bring up interface {0!r}' .format(interface)) raise logging.info('Starting network manager...') try: check_output(['/sbin/start', 'network-manager']) except CalledProcessError: logging.error('Unable to start network manager') raise # Verify that network interface is up for timeout in [2, 4, 8, 16, 32, 64]: logging.debug('Waiting ({0} seconds)...'.format(timeout)) time.sleep(timeout) success = ping(self.address) if success: break else: raise PingError(self.address, 'Some interface is still down') def _stop(self): """ Stop network manager """ logging.info('Stopping network manager...') try: check_output(['/sbin/stop', 'network-manager']) except CalledProcessError: logging.error('Unable to stop network manager') raise logging.info('Bringing all interfaces down...') for interface in self.interfaces: try: check_output(['/sbin/ifconfig', interface, 'down']) except CalledProcessError: logging.error('Unable to bring down interface {0!r}' .format(interface)) raise # Verify that network interface is down for timeout in [2, 4, 8]: logging.debug('Waiting ({0} seconds)...'.format(timeout)) time.sleep(timeout) success = ping(self.address) if not success: break else: raise PingError(self.address, 'Some interface is still up') def parse_args(): """ Parse command line options """ parser = ArgumentParser('Reboot networking interface ' 'and verify that is up again afterwards') parser.add_argument('-a', '--address', default='ubuntu.com', help=('Address to ping to verify that network connection is up ' "('%(default)s' by default)")) parser.add_argument('-o', '--output', default='/var/log', help='The path to the log directory. \ Default is /var/log') parser.add_argument('-t', '--times', type=int, default=1, help=('Number of times that the network interface has to be restarted ' '(%(default)s by default)')) log_levels = ['notset', 'debug', 'info', 'warning', 'error', 'critical'] parser.add_argument('--log-level', dest='log_level_str', default='notset', choices=log_levels, help=('Log level. ' 'One of {0} or {1} (%(default)s by default)' .format(', '.join(log_levels[:-1]), log_levels[-1]))) args = parser.parse_args() args.log_level = getattr(logging, args.log_level_str.upper()) return args def configure_logging(log_level, output): """ Configure logging """ logger = logging.getLogger() logger.setLevel(logging.DEBUG) # Log to sys.stderr using log level passed through command line if log_level != logging.NOTSET: log_handler = logging.StreamHandler() formatter = logging.Formatter('%(levelname)-8s %(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(log_level) logger.addHandler(log_handler) # Log to rotating file using DEBUG log level log_filename = os.path.join(output, '{0}.log'.format(os.path.basename(__file__))) rollover = os.path.exists(log_filename) log_handler = logging.handlers.RotatingFileHandler(log_filename, mode='a+', backupCount=3) formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') log_handler.setFormatter(formatter) log_handler.setLevel(logging.DEBUG) logger.addHandler(log_handler) if rollover: log_handler.doRollover() if __name__ == '__main__': try: sys.exit(main()) except PingError as exception: logging.error('Failed to ping {0!r}\n{1}' .format(exception.address, exception.reason)) sys.exit(1) 2013.com.canonical.certification.checkbox-0.4/bin/network_bandwidth_test0000775000175000017500000005101612320541306026275 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import re import sys import random import logging import subprocess from datetime import datetime, timedelta from time import sleep from logging import StreamHandler, FileHandler, Formatter from optparse import OptionParser from checkbox_support.lib.conversion import string_to_type class CommandException(Exception): pass class CommandOutput(object): def __init__(self, **attributes): self._attributes = attributes def __getattr__(self, name): if name in self._attributes: return self._attributes.get(name) return None class Command(object): # Name of the command to run name = None # Number of command line arguments argument_count = 0 # Option processing option_strings = {} option_defaults = {} # Ouput processing output_factory = CommandOutput output_patterns = {} # Convenient output patterns non_space = r"[^ ]+" def __init__(self, *arguments, **options): if len(arguments) != self.argument_count: raise TypeError("Invalid number of arguments: %d" % len(arguments)) self._arguments = arguments self._options = self.option_defaults.copy() for name, string in options.items(): if name not in self.option_strings: raise TypeError("Unknown option: %s" % name) self._options[name] = string def get_command(self): command = [self.name] for name, string in self._options.items(): # Match option from string if isinstance(string, bool): option = self.option_strings[name] else: option = self.option_strings[name] % string command.append(option) command.extend(self._arguments) return " ".join(command) def parse_lines(self, lines): attributes = {} for line in lines: # Match patterns from lines for name, pattern in self.output_patterns.items(): match = re.search(pattern, line) if match: attributes[name] = string_to_type(match.group(1)) return self.output_factory(**attributes) def parse_output(self, output): lines = output.split("\n") # Strip leading and trailing spaces lines = [l.strip() for l in lines] # Skip blank lines lines = [l for l in lines if l] return self.parse_lines(lines) def run(self): command = self.get_command() logging.debug("Running command: %s" % command) process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) error = process.stderr.read() if error: raise CommandException(error.decode("utf-8")) output = process.stdout.read() return self.parse_output(output.decode("utf-8")) class NetworkConfigOutput(CommandOutput): @property def speed(self): if self.name == "lo": return 10000 try: wireless = WirelessConfig(self.name).run() speed = wireless.bit_rate except CommandException: wired = WiredConfig(self.name).run() speed = wired.speed return speed / 1024 / 1024 class NetworkConfig(Command): name = "ifconfig" argument_count = 1 ipv4 = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" ipv6 = r"[\w:]+/\d+" mac_address = r"\w\w:\w\w:\w\w:\w\w:\w\w:\w\w" output_factory = NetworkConfigOutput output_patterns = { "name": r"(%s).*Link encap" % Command.non_space, "broadcast": r"Bcast:(%s)" % ipv4, "collisions": "collisions:(\d+)", "hwaddr": r"HWaddr (%s)" % mac_address, "inet_addr": r"inet addr:(%s)" % ipv4, "link_encap": r"Link encap:(%s)" % Command.non_space, "netmask": r"Mask:(%s)" % ipv4, "metric": r"Metric:(\d+)", "mtu": r"MTU:(\d+)", "rx_bytes": "RX bytes:(\d+)", "rx_dropped": "RX packets:.* dropped:(\d+)", "rx_errors": "RX packets:.* errors:(\d+)", "rx_frame": "RX packets:.* frame:(\d+)", "rx_overruns": "RX packets:.* overruns:(\d+)", "rx_packets": "RX packets:(\d+)", "tx_bytes": "TX bytes:(\d+)", "tx_carrier": "TX packets:.* carrier:(\d+)", "tx_dropped": "TX packets:.* dropped:(\d+)", "tx_errors": "TX packets:.* errors:(\d+)", "tx_overruns": "TX packets:.* overruns:(\d+)", "tx_packets": "TX packets:(\d+)", "txqueuelen": "txqueuelen:(\d+)"} class NetworkConfigs(Command): name = "ifconfig -a" def parse_output(self, output): outputs = [] for paragraph in output.split("\n\n"): if not paragraph: continue lines = paragraph.split("\n") name = re.split(r"\s+", lines[0])[0] config = NetworkConfig(name).parse_lines(lines) outputs.append(config) return outputs class WiredConfig(Command): name = "ethtool" argument_count = 1 output_patterns = { "advertised_auto_negotiation": r"Advertised auto-negotiation:\s+(.*)", "advertised_link_modes": r"Advertised link modes:\s+(.*)", "auto_negotiation": r"Auto-negotiation:\s+(.*)", "current_message_level": r"Current message level:\s+(.*)", "duplex": r"Duplex:\s+(.*)", "link_detected": r"Link detected:\s+(.*)", "phyad": r"PHYAD:\s+(.*)", "port": r"Port:\s+(.*)", "speed": r"Speed:\s+(.*)/s", "supported_auto_negotiation": r"Supports auto-negotiation:\s+(.*)", "supported_link_modes": r"Supported link modes:\s+(.*)", "supported_ports": r"Supported ports:\s+(.*)", "supports_wake_on": r"Supports Wake-on:\s+(.*)", "transceiver": r"Transceiver:\s+(.*)", "wake_on": r"Wake-on:\s+(.*)"} def parse_lines(self, lines): new_lines = [] # Skip header line for line in lines[1:]: if not re.search(r": ", line): new_lines[-1] += " " + line else: new_lines.append(line) return super(WiredConfig, self).parse_lines(new_lines) class WirelessConfig(Command): name = "iwconfig" argument_count = 1 fraction = r"\d+(/\d+)?" numeric = r"[\d\.]+" numeric_with_unit = r"%s( %s)?" % (numeric, Command.non_space) output_patterns = { "access_point": r"Access Point: (.*)", "bit_rate": r"Bit Rate[=:](%s)/s" % numeric_with_unit, "channel": r"Channel=(%s)" % Command.non_space, "essid": r"ESSID:\"?([^\"]+)\"?", "fragment_thr": r"Fragment thr:(\w+)", "frequency": r"Frequency:(%s)" % numeric_with_unit, "invalid_misc": r"Invalid misc:(\d+)", "link_quality": r"Link Quality[=:](%s)" % fraction, "missed_beacon": r"Missed beacon:(\d+)", "mode": r"Mode:(%s)" % Command.non_space, "noise_level": r"Noise level[=:](%s)" % numeric_with_unit, "power_management": r"Power Management:(.*)", "retry_limit": r"Retry limit:(\w+)", "rts_thr": r"RTS thr:(\w+)", "rx_invalid_crypt": r"Rx invalid crypt:(\d+)", "rx_invalid_frag": r"Rx invalid frag:(\d+)", "rx_invalid_nwid": r"Rx invalid nwid:(\d+)", "sensitivity": r"Sensitivity=(%s)" % fraction, "signal_level": r"Signal level[=:](%s)" % numeric_with_unit, "tx_excessive_retries": r"Tx excessive retries:(\d+)", "tx_power": r"Tx-Power=(%s)" % numeric_with_unit} class Ping(Command): name = "ping" argument_count = 1 option_strings = { "count": "-c %d", "flood": "-f", "interface": "-I %s", "quiet": "-q", "size": "-s %d", "ttl": "-t %d"} option_defaults = { "count": 1, "quiet": True} ms = r"\d+\.\d+" rtt = (ms, ms, ms, ms) output_patterns = { "packet_loss": r"(\d+)% packet loss,", "packets_received": r"(\d+) received,", "packets_transmitted": r"(\d+) packets transmitted,", "rtt_avg": r"rtt min/avg/max/mdev = %s/(%s)/%s/%s ms" % rtt, "rtt_max": r"rtt min/avg/max/mdev = %s/%s/(%s)/%s ms" % rtt, "rtt_mdev": r"rtt min/avg/max/mdev = %s/%s/%s/(%s) ms" % rtt, "rtt_min": r"rtt min/avg/max/mdev = (%s)/%s/%s/%s ms" % rtt, "time": r"time (\d+)ms"} def parse_lines(self, lines): # Skip ping lines return super(Ping, self).parse_lines(lines[-2:]) class PingLarge(Ping): # Some wired environments can handle the maximum ping packet # size, (65507+28)=65535 bytes. With a count of 191 packets, 65535 # bytes/packet, 8 bits/byte, the sum payload is 100137480 bits ~ # 100Mb. This is preferred and will be tried first. packet_size = 65507 packet_count = 191 option_defaults = { "count": packet_count, "flood": True, "quiet": True, "size": packet_size, "ttl": 1} class PingSmall(PingLarge): # If the large packet test was too lossy, we fall back to a packet # equal to the default MTU size of 1500, (1472+28)=1500 bytes. # With a count of 8334 packets, 1500 bytes/packet, 8 bits/byte, the # sum payload is 100008000 bits ~ 100Mb. packet_size = 1472 packet_count = 8334 option_defaults = PingLarge.option_defaults.copy() option_defaults.update({ "count": packet_count, "size": packet_size}) class PingHost(Command): output_patterns = { "host": r"(?:Host|Nmap scan report for) (%s)" % NetworkConfig.ipv4, "mac_address": r"MAC Address: (%s)" % NetworkConfig.mac_address} class PingScan(Command): name = "nmap -n -sP" argument_count = 1 def parse_lines(self, lines): hosts = [] host_lines = [] # Skip header lines for line in lines[1:]: host_lines.append(line) if line.startswith("MAC Address"): host = PingHost().parse_lines(host_lines) hosts.append(host) host_lines = [] return hosts class Ip(object): def __init__(self, address): self.address = address self.binary = self._address_to_binary(address) def __str__(self): return self.address def _address_to_binary(self, address): binary = 0 for position, part in enumerate(address.split(".")): if position >= 4: raise ValueError("Address contains more than four parts.") try: if not part: part = 0 else: part = int(part) if not 0 <= part < 256: raise ValueError except ValueError: raise ValueError("Address part out of range.") binary <<= 8 binary += part return binary def count_1_bits(self): ret = 0 num = self.binary while num > 0: num = num >> 1 ret += 1 return ret def count_0_bits(self): num = int(self.binary) if num < 0: raise ValueError("Only positive Numbers please: %s" % (num)) ret = 0 while num > 0: if num & 1 == 1: break num = num >> 1 ret += 1 return ret class IpRange(object): def __init__(self, address, netmask): self.address = Ip(address) self.netmask = Ip(netmask) self.prefix = self._netmask_to_prefix(self.netmask) def __str__(self): return "%s/%s" % (self.address, self.prefix) def _check_netmask(self, masklen): num = int(self.netmask.binary) bits = masklen # remove zero bits at the end while (num & 1) == 0: num = num >> 1 bits -= 1 if bits == 0: break # now check if the rest consists only of ones while bits > 0: if (num & 1) == 0: raise ValueError("Netmask %s can't be expressed as an prefix." % (hex(self.netmask.binary))) num = num >> 1 bits -= 1 def _netmask_to_prefix(self, netmask): netlen = netmask.count_0_bits() masklen = netmask.count_1_bits() self._check_netmask(masklen) return masklen - netlen def contains(self, address): address = Ip(address) if self.address.binary & self.netmask.binary \ == address.binary & self.netmask.binary: return True return False def scan(self, max=None): scan = PingScan(str(self)).run() targets = [s.host for s in scan] random.shuffle(targets) if max is not None: targets = targets[:max] return targets class NetworkManagerException(Exception): pass class NetworkManager(object): NM_SERVICE = "org.freedesktop.NetworkManager" NM_PATH = "/org/freedesktop/NetworkManager" NM_INTERFACE = NM_SERVICE NM_PATH_DEVICES = "/org/freedesktop/NetworkManager/Devices" NM_INTERFACE_DEVICES = "org.freedesktop.NetworkManager.Devices" NMI_SERVICE = "org.freedesktop.NetworkManagerInfo" NMI_PATH = "/org/freedesktop/NetworkManagerInfo" NMI_INTERFACE = NMI_SERVICE HAL_SERVICE = "org.freedesktop.Hal" HAL_PATH = "/org/freedesktop/Hal/Manager" HAL_INTERFACE = "org.freedesktop.Hal.Manager" HAL_INTERFACE_DEVICE = "org.freedesktop.Hal.Device" #http://projects.gnome.org/NetworkManager/developers/ #NetworkManager D-Bus API Specifications, look for the #NM_STATE enumeration to see which statuses indicate connection #established and put them in this list. "3" works for NM 0.7 #and 0.8, while "60" and "70" work for NM 0.9. STATES_CONNECTED = [3, 60, 70] def __init__(self): try: import dbus except ImportError: raise NetworkManagerException("Python module not found: dbus") try: self._bus = dbus.SystemBus() self.nm_object = self._bus.get_object(self.NM_SERVICE, self.NM_PATH) self.nm_service = dbus.Interface(self.nm_object, self.NM_INTERFACE) except dbus.exceptions.DBusException: raise NetworkManagerException("Failed to connect to dbus service") def is_connected(self): state = self.nm_service.state() return state in self.STATES_CONNECTED class Application(object): def __init__(self, targets, interfaces, scan): self.targets = targets self.interfaces = interfaces self.scan = scan def test_interface(self, interface, targets): logging.info("Testing %s at %s-Mbps", interface.name, interface.speed) for target in targets: ping = PingLarge(target, interface=interface.name) result = ping.run() if result.packet_loss: ping = PingSmall(target, interface=interface.name) result = ping.run() if result.packet_loss: logging.warning("SKIP: Non-zero packet loss (%s%%) " "for [%s] [%s]->[%s]", result.packet_loss, interface.name, interface.inet_addr, target) continue mbps = (8 * (ping.packet_size + 28) * ping.packet_count / result.time / 1000) percent = (100 * 8 * (ping.packet_size + 28) * ping.packet_count / result.time / 1000 / interface.speed) if percent >= 10: logging.info("PASS: Effective rate: %3.4f Mbps, " "%3.2f%% of theoretical max (%5.2f Mbps)", mbps, percent, interface.speed) return True else: logging.warning("Unacceptable network effective rate found for [%s]" % interface.name) logging.warning("Effective rate %3.4f Mbps, %3.2f%% of theoretical max (%5.2f Mbps)" % (mbps, percent, interface.speed)) return False def run(self): logging.debug("Acquiring network Interfaces") if self.interfaces: interfaces = [NetworkConfig(i).run() for i in self.interfaces] else: interfaces = NetworkConfigs().run() interfaces = [i for i in interfaces if i.inet_addr] for interface in interfaces: if not interface.inet_addr: logging.debug("No network address for [%s]", interface.name) continue targets = [] ip_range = IpRange(interface.inet_addr, interface.netmask) if self.targets: for target in self.targets: if ip_range.contains(target): targets.append(target) elif interface.name != "lo": targets = ip_range.scan(self.scan) logging.info("The following targets were found for %s:" % interface.name) for target in targets: logging.info("\t%s" % target) if not targets: logging.debug("No targets found for [%s]", interface.name) continue if not self.test_interface(interface, targets): return False return True class ApplicationManager(object): application_factory = Application default_log_level = "critical" default_scan = 1 default_timeout = 60 def get_parser(self, args): usage = "Usage: %prog [TARGETS]" parser = OptionParser(usage=usage) parser.add_option("-i", "--interface", dest="interfaces", action="append", type="string", default=[], help="Interface to test.") parser.add_option("-s", "--scan", default=self.default_scan, type="int", help="Number of targets to scan when not provided.") parser.add_option("-t", "--timeout", default=self.default_timeout, type="int", help="Time to wait for network manager to connect.") parser.add_option("-l", "--log", metavar="FILE", help="The file to write the log to.") parser.add_option("--log-level", default=self.default_log_level, help=("One of debug, info, warning, " "error or critical.")) return parser def check_uid(self): return os.getuid() == 0 def check_network(self, timeout): try: nm = NetworkManager() except NetworkManagerException: return True start = datetime.now() while True: if nm.is_connected(): return True if datetime.now() - start > timedelta(seconds=timeout): return False sleep(5) def create_application(self, args=sys.argv[1:]): parser = self.get_parser(args) (options, args) = parser.parse_args(args) log_level = logging.getLevelName(options.log_level.upper()) log_handlers = [] if options.log: log_filename = options.log log_handlers.append(FileHandler(log_filename)) else: log_handlers.append(StreamHandler()) # Logging setup format = ("%(asctime)s %(levelname)-8s %(message)s") date_format = '%Y-%m-%d %H:%M:%S' if log_handlers: for handler in log_handlers: handler.setFormatter(Formatter(format, date_format)) logging.getLogger().addHandler(handler) if log_level: logging.getLogger().setLevel(log_level) elif not logging.getLogger().handlers: logging.disable(logging.CRITICAL) if not self.check_uid(): parser.error("Must be run as root.") if not self.check_network(options.timeout): parser.error("Network devices must be configured and connected to a LAN segment before testing") targets = args return self.application_factory(targets, options.interfaces, options.scan) def main(): application_manager = ApplicationManager() application = application_manager.create_application() if not application.run(): return 1 return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/graphics_driver0000775000175000017500000003150412320541306024674 0ustar zygazyga00000000000000#!/usr/bin/env python3 #======================================================================== # # based on xlogparse # # DESCRIPTION # # Parses Xlog.*.log format files and allows looking up data from it # # AUTHOR # Bryce W. Harrington # # COPYRIGHT # Copyright (C) 2010-2012 Bryce W. Harrington # All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # #======================================================================== import re import sys import os from subprocess import Popen, PIPE, check_output, CalledProcessError class XorgLog(object): def __init__(self, logfile=None): self.modules = [] self.errors = [] self.warnings = [] self.info = [] self.notimpl = [] self.notices = [] self.cards = [] self.displays = {} self.xserver_version = None self.boot_time = None self.boot_logfile = None self.kernel_version = None self.video_driver = None self.xorg_conf_path = None self.logfile = logfile if logfile: self.parse(logfile) def parse(self, filename): self.displays = {} display = {} display_name = "Unknown" in_file = open(filename, errors='ignore') gathering_module = False found_ddx = False module = None for line in in_file.readlines(): m = re.search(r'\(..\)', line) if m: if gathering_module and module is not None: self.modules.append(module) gathering_module = False module = None m = re.search( '\(II\) Loading.*modules\/drivers\/(.+)_drv\.so', line) if m: found_ddx = True m = re.search(r'\(II\) Module (\w+):', line) if m: module = { 'name': m.group(1), 'vendor': None, 'version': None, 'class': None, 'abi_name': None, 'abi_version': None, 'ddx': found_ddx, } found_ddx = False gathering_module = True if gathering_module: m = re.search(r'vendor="(.*:?)"', line) if m: module['vendor'] = m.group(1) m = re.search(r'module version = (.*)', line) if m: module['version'] = m.group(1) if module['name'] == 'nvidia': try: version = check_output( "nvidia-settings -v", shell=True, universal_newlines=True) m = re.search(r'.*version\s+([0-9\.]+).*', version) if m: module['version'] = m.group(1) except CalledProcessError: pass m = re.search(r'class: (.*)', line) if m: module['class'] = m.group(1) m = re.search(r'ABI class:\s+(.*:?), version\s+(.*:?)', line) if m: if m.group(1)[:5] == "X.Org": module['abi_name'] = m.group(1)[6:] else: module['abi_name'] = m.group(1) module['abi_version'] = m.group(2) continue # General details m = re.search(r'Current Operating System: (.*)$', line) if m: uname = m.group(1) self.kernel_version = uname.split()[2] continue m = re.search(r'Kernel command line: (.*)$', line) if m: self.kernel_command_line = m.group(1) continue m = re.search(r'Build Date: (.*)$', line) if m: self.kernel_command_line = m.group(1) continue m = re.search(r'Log file: "(.*)", Time: (.*)$', line) if m: self.boot_logfile = m.group(1) self.boot_time = m.group(2) m = re.search(r'xorg-server ([^ ]+) .*$', line) if m: self.xserver_version = m.group(1) continue m = re.search(r'Using a default monitor configuration.', line) if m and self.xorg_conf_path is None: self.xorg_conf_path = 'default' continue m = re.search(r'Using config file: "(.*)"', line) if m: self.xorg_conf_path = m.group(1) continue # EDID and Modelines # We use this part to determine which driver is in use # For Intel / RADEON m = re.search(r'\(II\) (.*)\(\d+\): EDID for output (.*)', line) if m: self.displays[display_name] = display self.video_driver = m.group(1) display_name = m.group(2) display = {'Output': display_name} continue m = re.search( r'\(II\) (.*)\(\d+\): Assigned Display Device: (.*)$', line) if m: self.displays[display_name] = display self.video_driver = m.group(1) display_name = m.group(2) display = {'Output': display_name} continue # For NVIDIA m = re.search(r'\(II\) (.*)\(\d+\): Setting mode "(.*?):', line) if m: self.displays[display_name] = display self.video_driver = m.group(1) display_name = m.group(2) display = {'Output': display_name} continue # For 4th Intel on 3.11 m = re.search( r'\(II\) (.*)\(\d+\): switch to mode .* using (.*),', line) if m: self.displays[display_name] = display self.video_driver = m.group(1) display_name = m.group(2) display = {'Output': display_name} continue m = re.search( r'Manufacturer: (.*) *Model: (.*) *Serial#: (.*)', line) if m: display['display manufacturer'] = m.group(1) display['display model'] = m.group(2) display['display serial no.'] = m.group(3) m = re.search(r'EDID Version: (.*)', line) if m: display['display edid version'] = m.group(1) m = re.search(r'EDID vendor \"(.*)\", prod id (.*)', line) if m: display['vendor'] = m.group(1) display['product id'] = m.group(2) m = re.search( r'Max Image Size \[(.*)\]: *horiz.: (.*) *vert.: (.*)', line) if m: display['size max horizontal'] = "%s %s" % ( m.group(2), m.group(1)) display['size max vertical'] = "%s %s" % ( m.group(3), m.group(1)) m = re.search(r'Image Size: *(.*) x (.*) (.*)', line) if m: display['size horizontal'] = "%s %s" % (m.group(1), m.group(3)) display['size vertical'] = "%s %s" % (m.group(2), m.group(3)) m = re.search(r'(.*) is preferred mode', line) if m: display['mode preferred'] = m.group(1) m = re.search(r'Modeline \"(\d+)x(\d+)\"x([0-9\.]+) *(.*)$', line) if m: key = "mode %sx%s@%s" % (m.group(1), m.group(2), m.group(3)) display[key] = m.group(4) continue # Errors and Warnings m = re.search(r'\(WW\) (.*)$', line) if m: self.warnings.append(m.group(1)) continue m = re.search(r'\(EE\) (.*)$', line) if m: self.errors.append(m.group(1)) continue if display_name not in self.displays.keys(): self.displays[display_name] = display in_file.close() def errors_filtered(self): excludes = set([ 'error, (NI) not implemented, (??) unknown.', 'Failed to load module "fglrx" (module does not exist, 0)', 'Failed to load module "nv" (module does not exist, 0)', ]) return [err for err in self.errors if err not in excludes] def warnings_filtered(self): excludes = set([ 'warning, (EE) error, (NI) not implemented, (??) unknown.', 'The directory "/usr/share/fonts/X11/cyrillic" does not exist.', 'The directory "/usr/share/fonts/X11/100dpi/" does not exist.', 'The directory "/usr/share/fonts/X11/75dpi/" does not exist.', 'The directory "/usr/share/fonts/X11/100dpi" does not exist.', 'The directory "/usr/share/fonts/X11/75dpi" does not exist.', 'Warning, couldn\'t open module nv', 'Warning, couldn\'t open module fglrx', 'Falling back to old probe method for vesa', 'Falling back to old probe method for fbdev', ]) return [err for err in self.warnings if err not in excludes] def get_driver_info(xlog): '''Return the running driver and version''' print('-' * 13, 'VIDEO DRIVER INFORMATION', '-' * 13) if xlog.video_driver: for module in xlog.modules: if module['name'] == xlog.video_driver.lower(): print("Video Driver: %s" % module['name']) print("Driver Version: %s" % module['version']) print('\n') return 0 else: print("ERROR: No video driver loaded! Possibly in failsafe mode!", file=sys.stderr) return 1 def is_laptop(): return os.path.isdir('/proc/acpi/button/lid') def hybrid_graphics_check(xlog): '''Check for Hybrid Graphics''' card_id1 = re.compile('.*0300: *(.+):(.+) \(.+\)') card_id2 = re.compile('.*0300: *(.+):(.+)') cards_dict = {'8086': 'Intel', '10de': 'NVIDIA', '1002': 'AMD'} cards = [] drivers = [] formatted_cards = [] output = Popen(['lspci', '-n'], stdout=PIPE, universal_newlines=True) card_list = output.communicate()[0].split('\n') # List of discovered cards for line in card_list: m1 = card_id1.match(line) m2 = card_id2.match(line) if m1: id1 = m1.group(1).strip().lower() id2 = m1.group(2).strip().lower() id = id1 + ':' + id2 cards.append(id) elif m2: id1 = m2.group(1).strip().lower() id2 = m2.group(2).strip().lower() id = id1 + ':' + id2 cards.append(id) print('-' * 13, 'HYBRID GRAPHICS CHECK', '-' * 16) for card in cards: formatted_name = cards_dict.get(card.split(':')[0], 'Unknown') formatted_cards.append(formatted_name) print('Graphics Chipset: %s (%s)' % (formatted_name, card)) for module in xlog.modules: if module['ddx'] and module['name'] not in drivers: drivers.append(module['name']) print('Loaded DDX Drivers: %s' % ', '.join(drivers)) has_hybrid_graphics = (len(cards) > 1 and is_laptop() and (cards_dict.get('8086') in formatted_cards or cards_dict.get('1002') in formatted_cards)) print('Hybrid Graphics: %s' % (has_hybrid_graphics and 'yes' or 'no')) return 0 def main(): xlog = XorgLog("/var/log/Xorg.0.log") results = [] results.append(get_driver_info(xlog)) results.append(hybrid_graphics_check(xlog)) return 1 if 1 in results else 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/key_test0000775000175000017500000003100112320541306023340 0ustar zygazyga00000000000000#!/usr/bin/env python3 # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import os import sys import fcntl import gettext import struct import termios from gettext import gettext as _ from gi.repository import GObject from optparse import OptionParser EXIT_WITH_FAILURE = 1 EXIT_WITH_SUCCESS = 0 EXIT_TIMEOUT = 30 # Keyboard options from /usr/include/linux/kd.h K_RAW = 0x00 K_XLATE = 0x01 K_MEDIUMRAW = 0x02 K_UNICODE = 0x03 K_OFF = 0x04 KDGKBMODE = 0x4B44 KDSKBMODE = 0x4B45 def ioctl_p_int(fd, request, value=0): s = struct.pack("i", value) s2 = fcntl.ioctl(fd, request, s) (ret,) = struct.unpack("i", s2) # This always returns a tuple. return ret class Key: def __init__(self, codes, name=None): self.codes = codes self.name = name self.tested = False self.required = True @property def status(self): if not self.required: return _("Not required") if not self.tested: return _("Untested") return _("Tested") class Reporter(object): exit_code = EXIT_WITH_FAILURE def __init__(self, main_loop, keys, scancodes=False): self.main_loop = main_loop self.keys = keys self.scancodes = scancodes self.fileno = os.open("/dev/console", os.O_RDONLY) GObject.io_add_watch(self.fileno, GObject.IO_IN, self.on_key) # Set terminal attributes self.saved_attributes = termios.tcgetattr(self.fileno) attributes = termios.tcgetattr(self.fileno) attributes[3] &= ~(termios.ICANON | termios.ECHO) attributes[6][termios.VMIN] = 1 attributes[6][termios.VTIME] = 0 termios.tcsetattr(self.fileno, termios.TCSANOW, attributes) # Set keyboard mode self.saved_mode = ioctl_p_int(self.fileno, KDGKBMODE) mode = K_RAW if scancodes else K_MEDIUMRAW fcntl.ioctl(self.fileno, KDSKBMODE, mode) def _parse_codes(self, raw_bytes): """Parse the given string of bytes to scancodes or keycodes.""" if self.scancodes: return self._parse_scancodes(raw_bytes) else: return self._parse_keycodes(raw_bytes) def _parse_scancodes(self, raw_bytes): """Parse the bytes in raw_bytes into a scancode.""" index = 0 length = len(raw_bytes) while index < length: if (index + 1 < length and raw_bytes[index] == 0xE0): code = ((raw_bytes[index] << 8) | raw_bytes[index + 1]) index += 2 else: code = raw_bytes[0] index += 1 yield code def _parse_keycodes(self, raw_bytes): """Parse the bytes in raw_bytes into a keycode.""" index = 0 length = len(raw_bytes) while index < length: if (index + 2 < length and (raw_bytes[index] & 0x7f) == 0 and (raw_bytes[index + 1] & 0x80) != 0 and (raw_bytes[index + 2] & 0x80) != 0): code = (((raw_bytes[index + 1] & 0x7f) << 7) | (raw_bytes[2] & 0x7f)) index += 3 else: code = (raw_bytes[0] & 0x7f) index += 1 yield code @property def required_keys_tested(self): """Returns True if all keys marked as required have been tested""" return all([key.tested for key in self.keys if key.required]) def show_text(self, string): pass def quit(self, exit_code=EXIT_WITH_FAILURE): self.exit_code = exit_code termios.tcsetattr(self.fileno, termios.TCSANOW, self.saved_attributes) fcntl.ioctl(self.fileno, KDSKBMODE, self.saved_mode) # FIXME: Having a reference to the mainloop is suboptimal. self.main_loop.quit() def found_key(self, key): key.tested = True def toggle_key(self, key): key.required = not key.required key.tested = False def on_key(self, source, cb_condition): raw_bytes = os.read(source, 18) for code in self._parse_codes(raw_bytes): if code == 1: # Check for ESC key pressed self.show_text(_("Test cancelled")) self.quit() elif code > 1 and code < 10: # Check for number to skip self.toggle_key(self.keys[code - 2]) else: # Check for other key pressed for key in self.keys: if code in key.codes: self.found_key(key) break return True class CLIReporter(Reporter): def __init__(self, *args, **kwargs): super(CLIReporter, self).__init__(*args, **kwargs) self.show_text(_("Please press each key on your keyboard.")) self.show_text(_("I will exit automatically once all keys " "have been pressed.")) self.show_text(_("If your keyboard lacks one or more keys, " "press its number to skip testing that key.")) self.show_text(_("You can also close me by pressing ESC or Ctrl+C.")) self.show_keys() def show_text(self, string): sys.stdout.write(string + "\n") sys.stdout.flush() def show_keys(self): self.show_text("---") for index, key in enumerate(self.keys): self.show_text( "%(number)d - %(key)s - %(status)s" % {"number": index + 1, "key": key.name, "status": key.status}) def found_key(self, key): super(CLIReporter, self).found_key(key) self.show_text( _("%(key_name)s key has been pressed" % {'key_name': key.name})) self.show_keys() if self.required_keys_tested: self.show_text(_("All required keys have been tested!")) self.quit(EXIT_WITH_SUCCESS) def toggle_key(self, key): super(CLIReporter, self).toggle_key(key) self.show_keys() class GtkReporter(Reporter): def __init__(self, *args, **kwargs): super(GtkReporter, self).__init__(*args, **kwargs) from gi.repository import Gdk, Gtk # Initialize GTK constants self.ICON_SIZE = Gtk.IconSize.BUTTON self.ICON_TESTED = Gtk.STOCK_YES self.ICON_UNTESTED = Gtk.STOCK_INDEX self.ICON_NOT_REQUIRED = Gtk.STOCK_REMOVE self.button_factory = Gtk.Button self.hbox_factory = Gtk.HBox self.image_factory = Gtk.Image self.label_factory = Gtk.Label self.vbox_factory = Gtk.VBox # Create GTK window. window = Gtk.Window() window.set_type_hint(Gdk.WindowType.TOPLEVEL) window.set_size_request(100, 100) window.set_resizable(False) window.set_title(_("Key test")) window.connect("delete_event", lambda w, e: self.quit()) window.connect( "key-release-event", lambda w, k: k.keyval == Gdk.KEY_Escape and self.quit()) window.show() # Add common widgets to the window. vbox = self._add_vbox(window) self.label = self._add_label(vbox) button_hbox = self._add_hbox(vbox) validation_hbox = self._add_hbox(vbox) skip_hbox = self._add_hbox(vbox) exit_button = self._add_button(vbox, _("_Exit"), True) exit_button.connect("clicked", lambda w: self.quit()) # Add widgets for each key. self.icons = {} for key in self.keys: stock = getattr(Gtk, "STOCK_MEDIA_%s" % key.name.upper(), None) if stock: self._add_image(button_hbox, stock) else: self._add_label(button_hbox, key.name) self.icons[key] = self._add_image(validation_hbox, Gtk.STOCK_INDEX) button = self._add_button(skip_hbox, _("Skip")) button.connect("clicked", self.on_skip, key) self.show_text(_("Please press each key on your keyboard.")) self.show_text(_("If a key is not present in your keyboard, " "press the 'Skip' button below it to remove it " "from the test.")) def _add_button(self, context, label, use_underline=False): button = self.button_factory(label=label, use_underline=use_underline) context.add(button) button.show() return button def _add_hbox(self, context, spacing=4): hbox = self.hbox_factory() context.add(hbox) hbox.set_spacing(4) hbox.show() return hbox def _add_image(self, context, stock): image = self.image_factory(stock=stock, icon_size=self.ICON_SIZE) context.add(image) image.show() return image def _add_label(self, context, text=None): label = self.label_factory() context.add(label) label.set_size_request(0, 0) label.set_line_wrap(True) if text: label.set_text(text) label.show() return label def _add_vbox(self, context): vbox = self.vbox_factory() vbox.set_homogeneous(False) vbox.set_spacing(8) context.add(vbox) vbox.show() return vbox def show_text(self, string): self.label.set_text(self.label.get_text() + "\n" + string) def check_keys(self): if self.required_keys_tested: self.show_text(_("All required keys have been tested!")) self.quit(EXIT_WITH_SUCCESS) def found_key(self, key): super(GtkReporter, self).found_key(key) self.icons[key].set_from_stock(self.ICON_TESTED, size=self.ICON_SIZE) self.check_keys() def on_skip(self, sender, key): self.toggle_key(key) if key.required: stock_icon = self.ICON_UNTESTED else: stock_icon = self.ICON_NOT_REQUIRED self.icons[key].set_from_stock(stock_icon, self.ICON_SIZE) self.check_keys() def main(args): gettext.textdomain("checkbox") usage = """\ Usage: %prog [OPTIONS] CODE... Syntax for codes: 57435 - Decimal code without name 0160133:Super - Octal code with name 0xe05b,0xe0db:Super - Multiple hex codes with name Hint to find codes: The showkey command can show keycodes and scancodes. """ parser = OptionParser(usage=usage) parser.add_option("-i", "--interface", default="auto", help="Interface to use: cli, gtk or auto") parser.add_option("-s", "--scancodes", default=False, action="store_true", help="Test for scancodes instead of keycodes.") (options, args) = parser.parse_args(args) # Get reporter factory from options or environment. if options.interface == "auto": if "DISPLAY" in os.environ: reporter_factory = GtkReporter else: reporter_factory = CLIReporter elif options.interface == "cli": reporter_factory = CLIReporter elif options.interface == "gtk": reporter_factory = GtkReporter else: parser.error("Unsupported interface: %s" % options.interface) if not args: parser.error("Must specify codes to test.") # Get keys from command line arguments. keys = [] for codes_name in args: if ":" in codes_name: codes, name = codes_name.split(":", 1) else: codes, name = codes_name, codes_name # Guess the proper base from the string. codes = [int(code, 0) for code in codes.split(",")] key = Key(codes, name) keys.append(key) main_loop = GObject.MainLoop() try: reporter = reporter_factory(main_loop, keys, options.scancodes) except: parser.error("Failed to initialize interface: %s" % options.interface) GObject.timeout_add_seconds(EXIT_TIMEOUT, reporter.quit) try: main_loop.run() except KeyboardInterrupt: reporter.show_text(_("Test interrupted")) reporter.quit() return reporter.exit_code if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/sources_test0000775000175000017500000000123712320541306024243 0ustar zygazyga00000000000000#!/bin/bash result=0 sources_list=$1 repositories=$2 if [ -z "$sources_list" ]; then echo "Must provide sources list location, e.g. /etc/apt/sources.list" exit 1 fi if [ -z "$repositories" ]; then echo "Must provide list of repositories to check for, e.g. 'deb http://gb.archive.ubuntu.com/ubuntu/ precise multiverse, deb http://gb.archive.ubuntu.com/ubuntu/ precise-updates multiverse'" exit 1 fi IFS=$',' for repository in $repositories; do if grep -q "$repository" "$sources_list"; then echo "$repository found in $sources_list" else echo "$repository not found in $sources_list" result=1 fi done exit $result 2013.com.canonical.certification.checkbox-0.4/bin/memory_compare0000775000175000017500000000563312320541306024543 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys from checkbox_support.parsers.lshwjson import LshwJsonParser from checkbox_support.parsers.meminfo import MeminfoParser from subprocess import check_output, PIPE THRESHOLD = 25 class LshwJsonResult: memory_reported = 0 banks_reported = 0 def addHardware(self, hardware): if hardware['id'] == 'memory': self.memory_reported += int(hardware.get('size', 0)) elif 'bank' in hardware['id']: self.banks_reported += int(hardware.get('size', 0)) def get_installed_memory_size(): lshw = LshwJsonParser(check_output(['lshw','-json'], universal_newlines=True, stderr=PIPE)) result = LshwJsonResult() lshw.run(result) if result.memory_reported: return result.memory_reported else: return result.banks_reported class MeminfoResult: memtotal = 0 def setMemory(self, memory): self.memtotal = memory['total'] def get_visible_memory_size(): parser = MeminfoParser(open('/proc/meminfo')) result = MeminfoResult() parser.run(result) return result.memtotal def get_threshold(installed_memory): GB = 1024**3 if installed_memory <= 2 * GB: return 25 elif installed_memory <= 6 * GB: return 20 else: return 10 def main(): if os.geteuid() != 0: print("This script must be run as root.", file=sys.stderr) return 1 installed_memory = get_installed_memory_size() visible_memory = get_visible_memory_size() threshold = get_threshold(installed_memory) difference = installed_memory - visible_memory try: percentage = difference / installed_memory * 100 except ZeroDivisionError: print("Results:") print("\t/proc/meminfo reports:\t%s kB" % (visible_memory / 1024), file=sys.stderr) print("\tlshw reports:\t%s kB" % (installed_memory / 1024), file=sys.stderr) print("\nFAIL: Either lshw or /proc/meminfo returned a memory size of 0 kB", file=sys.stderr) return 1 if percentage <= threshold: print("Results:") print("\t/proc/meminfo reports:\t%s kB" % (visible_memory / 1024)) print("\tlshw reports:\t%s kB" % (installed_memory / 1024)) print("\nPASS: Meminfo reports %d bytes less than lshw, a difference of %.2f%%. This is less than the %d%% variance allowed." % (difference, percentage, threshold)) return 0 else: print("Results") print("\t/proc/meminfo reports:\t%s kB" % (visible_memory / 1024), file=sys.stderr) print("\tlshw reports:\t%s kB" % (installed_memory / 1024), file=sys.stderr) print("\nFAIL: Meminfo reports %d bytes less than lshw, a difference of %.2f%%. Only a variance of %d%% in reported memory is allowed." % (difference, percentage, threshold), file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/keyboard_test0000775000175000017500000000362612320541306024364 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys from gettext import gettext as _ import gettext def cli_prompt(): import termios limit = 50 separator = ord("\n") fileno = sys.stdin.fileno() saved_attributes = termios.tcgetattr(fileno) attributes = termios.tcgetattr(fileno) attributes[3] = attributes[3] & ~(termios.ICANON) attributes[6][termios.VMIN] = 1 attributes[6][termios.VTIME] = 0 termios.tcsetattr(fileno, termios.TCSANOW, attributes) sys.stdout.write(_("Enter text:\n")) input = "" try: while len(input) < limit: ch = str(sys.stdin.read(1)) if ord(ch) == separator: break input += ch finally: termios.tcsetattr(fileno, termios.TCSANOW, saved_attributes) def gtk_prompt(): from gi.repository import Gtk, Gdk # create a new window window = Gtk.Window() window.set_type_hint(Gdk.WindowType.TOPLEVEL) window.set_size_request(200, 100) window.set_resizable(False) window.set_title(_("Type Text")) window.connect("delete_event", lambda w, e: Gtk.main_quit()) vbox = Gtk.VBox() vbox.set_homogeneous(False) vbox.set_spacing(0) window.add(vbox) vbox.show() entry = Gtk.Entry() entry.set_max_length(50) vbox.pack_start(entry, True, True, 0) entry.show() hbox = Gtk.HBox() hbox.set_homogeneous(False) hbox.set_spacing(0) vbox.add(hbox) hbox.show() button = Gtk.Button(stock=Gtk.STOCK_CLOSE) button.connect("clicked", lambda w: Gtk.main_quit()) vbox.pack_start(button, False, False, 0) button.set_can_default(True) button.grab_default() button.show() window.show() Gtk.main() def main(args): gettext.textdomain("checkbox") if "DISPLAY" in os.environ: gtk_prompt() else: cli_prompt() return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/memory_info0000775000175000017500000000130412320541306024037 0ustar zygazyga00000000000000#!/usr/bin/env python3 import re import sys def get_meminfo(): meminfo = {} for line in open("/proc/meminfo").readlines(): match = re.match(r"(.*):\s+(.*)", line) if match: key = match.group(1) value = match.group(2) meminfo[key] = value return meminfo def main(args): meminfo = get_meminfo() amount, units = meminfo["MemTotal"].split() amount = float(amount) next_units = {'kB': 'MB', 'MB': 'GB'} while amount > 1024: amount = amount / 1024 units = next_units[units] print("%.1f %s" % (amount, units)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/gst_pipeline_test0000775000175000017500000000437312320541306025246 0ustar zygazyga00000000000000#!/usr/bin/env python3 from argparse import ArgumentParser import logging import re import os import sys import time from gi.repository import Gst from gi.repository import GLib from subprocess import check_output def check_state(device): """Checks whether the sink is available for the given device. """ sink_info = check_output(['pacmd', 'list-sinks'], universal_newlines=True) data = sink_info.split("\n") try: device_name = re.findall(".*name:\s.*%s.*" % device, sink_info)[0].lstrip() sink = re.findall(".*name:\s<(.*%s.*)>" % device, sink_info)[0].lstrip() status = data[data.index("\t" + device_name) + 3] except (IndexError, ValueError): logging.error("Failed to find status for device: %s" % device) return False os.environ['PULSE_SINK'] = sink logging.info("[ Pulse sink ]".center(80, '=')) logging.info("Device: %s %s" % (device_name.strip(), status.strip())) return status def main(): parser = ArgumentParser(description='Simple GStreamer pipeline player') parser.add_argument('PIPELINE', help='Quoted GStreamer pipeline to launch') parser.add_argument('-t', '--timeout', type=int, required=True, help='Timeout for running the pipeline') parser.add_argument('-d', '--device', type=str, help="Device to check for status") args = parser.parse_args() logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO, stream=sys.stdout) exit_code = 0 if args.device: if not check_state(args.device): exit_code = 1 Gst.init(None) try: print("Attempting to initialize Gstreamer pipeline: {}".format( args.PIPELINE)) element = Gst.parse_launch(args.PIPELINE) except GLib.GError as error: print("Specified pipeline couldn't be processed.") print("Error when processing pipeline: {}".format(error)) #Exit harmlessly return(2) print("Pipeline initialized, now starting playback.") element.set_state(Gst.State.PLAYING) if args.timeout: time.sleep(args.timeout) element.set_state(Gst.State.NULL) return exit_code if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/resolution_test0000775000175000017500000000245712320541306024770 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys from argparse import ArgumentParser from gi.repository import Gdk def check_resolution(): screen = Gdk.Screen.get_default() n = screen.get_n_monitors() for i in range(n): geom = screen.get_monitor_geometry(i) print('Monitor %d:' % (i + 1)) print(' %d x %d' % (geom.width, geom.height)) def compare_resolution(min_h, min_v): # Evaluate just the primary display screen = Gdk.Screen.get_default() geom = screen.get_monitor_geometry(screen.get_primary_monitor()) print("Minimum acceptable display resolution: %d x %d" % (min_h, min_v)) print("Detected display resolution: %d x %d" % (geom.width, geom.height)) return geom.width >= min_h and geom.height >= min_v def main(): parser = ArgumentParser() parser.add_argument("--horizontal", type=int, default=0, help="Minimum acceptable horizontal resolution.") parser.add_argument("--vertical", type=int, default=0, help="Minimum acceptable vertical resolution.") args = parser.parse_args() if (args.horizontal > 0) and (args.vertical > 0): return not compare_resolution(args.horizontal, args.vertical) else: check_resolution() return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/lamp_test0000775000175000017500000000126312320541306023510 0ustar zygazyga00000000000000#!/bin/bash # # Test LAMP by checking Apache, MySQL, and PHP # Requires: apache2, php5-mysql, libapache2-mod-php5, mysql-server # # Check Apache is running; requires network connection so verify that check=`ping -c 2 www.ubuntu.com |grep "2 received"` if [ -n "$check" ]; then run1=`w3m http://127.0.0.1/ | grep "404"` if [ -n "$run1" ]; then echo "FAIL: apache is not running." exit 1 fi fi # Check if MySQL server is running run2=`netstat -tap | grep mysql` if [ -z "$run2" ]; then echo "FAIL: mysql is not running." exit 1 fi # Check PHP run3=`php -r 'phpinfo();' | grep 'PHP License'` if [ -z "$run3" ]; then echo "FAIL: php is not running." exit 1 fi exit 0 2013.com.canonical.certification.checkbox-0.4/bin/memory_test0000775000175000017500000002073712320541306024076 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import sys import re from argparse import ArgumentParser from subprocess import Popen, PIPE class MemoryTest(): def __init__(self): self.free_memory = 0 self.system_memory = 0 self.swap_memory = 0 self.process_memory = 0 self.is_process_limited = False @property def threaded_memtest_script(self): directory = os.path.dirname(os.path.abspath(__file__)) return os.path.join(directory, "threaded_memtest") def _get_memory(self): mem_info = open("/proc/meminfo", "r") try: while True: line = mem_info.readline() if line: tokens = line.split() if len(tokens) == 3: if "MemTotal:" == tokens[0].strip(): self.system_memory = \ int(tokens[1].strip()) // 1024 elif tokens[0].strip() in ["MemFree:", "Cached:", "Buffers:"]: self.free_memory += \ int(tokens[1].strip()) // 1024 elif "SwapTotal:" == tokens[0].strip(): self.swap_memory = \ int(tokens[1].strip()) // 1024 else: break except Exception as e: print("ERROR: Unable to get data from /proc/meminfo", file=sys.stderr) print(e, file=sys.stderr) finally: mem_info.close() def _command(self, command, shell=True): proc = Popen(command, shell=shell, stdout=PIPE, stderr=PIPE) return proc def _command_out(self, command, shell=True): proc = self._command(command, shell) return proc.communicate()[0].strip() def get_limits(self): self._get_memory() print("System Memory: %u MB" % self.system_memory) print("Free Memory: %u MB" % self.free_memory) print("Swap Memory: %u MB" % self.swap_memory) if self.system_memory == 0: print("ERROR: could not determine system RAM", file=sys.stderr) return False # Process Memory self.process_memory = self.free_memory try: arch = self._command_out("arch").decode() if (re.match(r"(i[0-9]86|s390|arm.*)", arch) and self.free_memory > 1024): self.is_process_limited = True self.process_memory = 1024 # MB, due to 32-bit address space print("%s arch, Limiting Process Memory: %u" % ( arch, self.process_memory)) # others? what about PAE kernel? except Exception as e: print("ERROR: could not determine system architecture via arch", file=sys.stderr) print(e, file=sys.stderr) return False return True def run(self): PASSED = 0 FAILED = 1 limits = self.get_limits() if not limits: return FAILED # if process memory is limited, run multiple processes if self.is_process_limited: print("Running Multiple Process Memory Test") if not self.run_multiple_process_test(): return FAILED else: print("Running Single Process Memory Test") if not self.run_single_process_test(): return FAILED # otherwised, passed return PASSED def run_single_process_test(self): if not self.run_threaded_memory_test(): return False return True def run_multiple_process_test(self): processes = self.free_memory // self.process_memory # if not swap-less, add a process to hit swap if not self.swap_memory == 0: processes += 1 # check to make sure there's enough swap required_memory = self.process_memory * processes if required_memory > self.system_memory + self.swap_memory: print("ERROR: this test requires a minimum of %u KB of swap " "memory (%u configured)" % ( required_memory - self.system_memory, self.swap_memory), file=sys.stderr) print("Testing memory with %u processes" % processes) print("Running threaded memory test:") run_time = 60 # sec. if not self.run_processes(processes, "%s -qpv -m%um -t%u" % ( self.threaded_memtest_script, self.process_memory, run_time)): print("Multi-process, threaded memory Test FAILED", file=sys.stderr) return False return True def run_threaded_memory_test(self): # single-process threaded test print("Starting Threaded Memory Test") # run for Free Memory plus the lessor of 5% or 1GB memory = (self.free_memory * 5) / 100 if memory > 1024: # MB memory = 1024 # MB memory = memory + self.free_memory print("Running for %d MB total memory" % memory) # run a test that will swap if not self.swap_memory == 0: # is there enough swap memory for the test? if memory > self.system_memory + self.swap_memory: print("ERROR: this test requires a minimum of %u KB of swap " "memory (%u configured)" % (memory - self.system_memory, self.swap_memory), file=sys.stderr) return False # otherwise run_time = 60 # sec. print("Running for more than free memory at %u MB for %u sec." % ( memory, run_time)) command = "%s -qpv -m%um -t%u" % ( self.threaded_memtest_script, memory, run_time) print("Command is: %s" % command) process = self._command(command) process.communicate() if process.returncode != 0: print("%s returned code %s" % (self.threaded_memtest_script, process.returncode), file=sys.stderr) print("More Than Free Memory Test failed", file=sys.stderr) return False print("More than free memory test complete.") # run again for 15 minutes print("Running for free memory") process = self._command("%s -qpv" % self.threaded_memtest_script) process.communicate() if process.returncode != 0: print("Free Memory Test failed", file=sys.stderr) else: print("Free Memory Test succeeded") sys.stdout.flush() return (process.returncode == 0) def run_processes(self, number, command): passed = True pipe = [] for i in range(number): pipe.append(self._command(command)) print("Started: process %u pid %u: %s" % (i, pipe[i].pid, command)) sys.stdout.flush() waiting = True while waiting: waiting = False for i in range(number): if pipe[i]: line = pipe[i].communicate()[0] if line and len(line) > 1: print("process %u pid %u: %s" % (i, pipe[i].pid, line)) sys.stdout.flush() if pipe[i].poll() == -1: waiting = True else: return_value = pipe[i].poll() if return_value != 0: print("ERROR: process %u pid %u retuned %u" % (i, pipe[i].pid, return_value), file=sys.stderr) passed = False print("process %u pid %u returned success" % (i, pipe[i].pid)) pipe[i] = None sys.stdout.flush() return passed def main(args): parser = ArgumentParser() parser.add_argument("-q", "--quiet", action="store_true", help="Suppress output.") args = parser.parse_args(args) if args.quiet: sys.stdout = open(os.devnull, 'a') sys.stderr = open(os.devnull, 'a') test = MemoryTest() return test.run() if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/gputest_benchmark0000775000175000017500000000150312320541306025222 0ustar zygazyga00000000000000#!/usr/bin/python3 # This file is part of Checkbox. # # Copyright 2013 Canonical Ltd. # Written by: # Sylvain Pineau # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import sys from checkbox_support.scripts.gputest_benchmark import main if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/brightness_test0000775000175000017500000001304412320541306024727 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # brightness_test # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . import sys import os import time from sys import stdout, stderr from glob import glob class Brightness(object): def __init__(self, path='/sys/class/backlight'): self.sysfs_path = path self._interfaces = self._get_interfaces_from_path() def read_value(self, path): '''Read the value from a file''' # See if the source is a file or a file object # and act accordingly file = path if file == None: lines_list = [] else: # It's a file if not hasattr(file, 'write'): myfile = open(file, 'r') lines_list = myfile.readlines() myfile.close() # It's a file object else: lines_list = file.readlines() return int(''.join(lines_list).strip()) def write_value(self, value, path, test=None): '''Write a value to a file''' value = '%d' % value # It's a file if not hasattr(path, 'write'): if test: path = open(path, 'a') else: path = open(path, 'w') path.write(value) path.close() # It's a file object else: path.write(value) def get_max_brightness(self, path): full_path = os.path.join(path, 'max_brightness') return self.read_value(full_path) def get_actual_brightness(self, path): full_path = os.path.join(path, 'actual_brightness') return self.read_value(full_path) def get_last_set_brightness(self, path): full_path = os.path.join(path, 'brightness') return self.read_value(full_path) def _get_interfaces_from_path(self): '''check all the files in a directory looking for quirks''' interfaces = [] if os.path.isdir(self.sysfs_path): for d in glob(os.path.join(self.sysfs_path, '*')): if os.path.isdir(d): interfaces.append(d) return interfaces def get_best_interface(self): '''Get the best acpi interface''' # Follow the heuristic in https://www.kernel.org/doc/Documentation/ #ABI/stable/sysfs-class-backlight if len(self._interfaces) == 0: return None else: interfaces = {} for interface in self._interfaces: try: with open(interface + '/type') as type_file: iface_type = type_file.read().strip() except IOError: continue interfaces[iface_type] = interface if interfaces.get('firmware'): return interfaces.get('firmware') elif interfaces.get('platform'): return interfaces.get('platform') elif interfaces.get('raw'): return interfaces.get('raw') else: fallback_type = sorted(interfaces.keys())[0] print("WARNING: no interface of type firmware/platform/raw " "found. Using {} of type {}".format( interfaces[fallback_type], fallback_type)) return interfaces[fallback_type] def was_brightness_applied(self, interface): '''See if the selected brightness was applied Note: this doesn't guarantee that screen brightness changed. ''' if (self.get_actual_brightness(interface) != self.get_last_set_brightness(interface)): return False else: return True def main(): brightness = Brightness() # Make sure that we have root privileges if os.geteuid() != 0: print('Error: please run this program as root', file=sys.stderr) exit(1) interface = brightness.get_best_interface() # If no backlight interface can be found if not interface: exit(1) # Get the current brightness which we can restore later current_brightness = brightness.get_actual_brightness(interface) # Get the maximum value for brightness max_brightness = brightness.get_max_brightness(interface) # Set the brightness to half the max value brightness.write_value(max_brightness / 2, os.path.join(interface, 'brightness')) # Check that "actual_brightness" reports the same value we # set "brightness" to exit_status = not brightness.was_brightness_applied(interface) # Wait a little bit before going back to the original value time.sleep(2) # Set the brightness back to its original value brightness.write_value(current_brightness, os.path.join(interface, 'brightness')) exit(exit_status) if __name__ == '__main__': main() 2013.com.canonical.certification.checkbox-0.4/bin/ansi_parser0000775000175000017500000001077212320541306024033 0ustar zygazyga00000000000000#!/usr/bin/python3 import sys from optparse import OptionParser def parse_buffer(input): output = [""] row = -1 col = 0 escape = "" saved = [0, 0] for ch in input: if ord(ch) == 27 or len(escape) > 0: # On ESC if chr(27) in [escape, ch]: escape = "" if ch == "c": output = [""] row = -1 col = 0 saved = [0, 0] elif ch == "D": row += 1 if row == 0: row = -1 output.append("") elif ch == "M": row -= 1 if row < -len(output): output = [""] + output elif ch == "7": saved = [row + len(output), col] elif ch == "8": [row, col] = saved row -= len(output) elif ord(ch) in [27, 91]: escape = ch continue # Just after hitting the extended ESC marker elif escape == "[": escape = "" if ch in "0123456789;": escape += ch continue elif ch in "Hf": opts = escape.split(";") + ["", ""] row = -len(output) + max(0, int("0" + opts[0]) - 1) col = max(0, int("0" + opts[1]) - 1) elif ch in "s": saved = [row + len(output), col] elif ch in "u": [row, col] = saved row -= len(output) elif ch in "K": if escape == "1": output[row] = " " * (col + 1) + output[row][col + 1:] elif escape == "2": output[row] = "" else: output[row] = output[row][:col] elif ch in "J": if len(escape) == 0: output = output[:row] + [""] else: for i in range(row + len(output) + 1): output[i] = "" elif ch in "A": row -= max(1, int("0" + escape.split(";")[0])) if row <= len(output): row = -len(output) elif ch in "B": row += max(1, int("0" + escape.split(";")[0])) while row >= 0: output.append("") row -= 1 elif ch in "C": col += max(1, int("0" + escape.split(";")[0])) elif ch in "D": col = max(0, col - max(1, int("0" + escape.split(";")[0]))) escape = "" continue # Control char if ch in "\r\n\f\t\b": if ch == "\r": col = 0 if ch in "\n\f": row += 1 if row == 0: row = -1 output.append("") col = 0 if ch == "\t": col = (col + 8) & ~7 if ch == "\b": col = max(0, col - 1) continue # Keep to ascii if ord(ch) not in range(32, 127): ch = "?" if len(output[row]) < col: output[row] += " " * (col - len(output[row])) output[row] = output[row][:col] + ch + output[row][col + 1:] col += 1 return "\n".join(output) def parse_file(file): output = file.read() return parse_buffer(output) def parse_filename(filename): file = open(filename) try: output = parse_file(file) finally: file.close() return output def main(args): usage = "Usage: %prog [OPTIONS] [FILE...]" parser = OptionParser(usage=usage) parser.add_option("-o", "--output", metavar="FILE", help="File where to output the result.") (options, args) = parser.parse_args(args) # Write to stdout if not options.output or options.output == "-": output = sys.stdout # Or from given option else: output = open(options.output, "w") # Read from sdin if not args or (len(args) == 1 and args[0] == "-"): output.write(parse_file(sys.stdin)) # Or from filenames given as arguments else: for arg in args: output.write(parse_filename(arg)) if options.output and options.output != "-": output.close() return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/bin/screenshot_validation0000775000175000017500000001241412320541306026107 0ustar zygazyga00000000000000#!/usr/bin/env python2.7 # Copyright 2014 Canonical Ltd. # Written by: # Sylvain Pineau # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import absolute_import, print_function import argparse import imghdr import os import cv2 def create_capture(args): try: device_no = int(os.path.realpath(args.device)[-1]) except ValueError: raise SystemExit( "ERROR: video source not found: {}".format(args.device)) cap = cv2.VideoCapture(device_no) # The camera driver will adjust the capture size cap.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, args.width) cap.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, args.height) if cap is None or not cap.isOpened(): raise SystemExit( "ERROR: unable to open video source: {}".format(args.device)) return cap parser = argparse.ArgumentParser( description=''' Automatically validates a screenshot captured with an external camera using OpenCV ORB detection and a FLANN Matcher (Fast Approximate Nearest Neighbor Search Library) Put your camera (HD recommended) in front of your monitor. A query image (the INPUT positional argument) is displayed on your primary device and several captures (see -F) are analyzed to find a positive match. On success returns 0. Otherwise a non-zero value is returned and a diagnostic message is printed on standard error. ''', formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument('input', metavar='INPUT', help='Input file to use as query image') parser.add_argument('--min_matches', type=int, default=20, help='Minimum threshold value to validate a \ positive match') parser.add_argument('-F', '--frames', type=int, default=10, help='Set the number of frames to capture and analyze \ Minimum: 3') parser.add_argument('-d', '--device', default='/dev/video0', help='Set the device to use') parser.add_argument('--height', type=int, default=900, help='Set the capture height') parser.add_argument('--width', type=int, default=1600, help='Set the capture width') parser.add_argument('-o', '--output', default=None, help='Save the screenshot to the specified filename') args = parser.parse_args() if args.frames < 3: parser.print_help() raise SystemExit(1) if not imghdr.what(args.input): raise SystemExit( "ERROR: unable to read the input file: {}".format(args.input)) queryImage = cv2.imread(args.input, cv2.CV_LOAD_IMAGE_GRAYSCALE) cv2.namedWindow("test", cv2.WND_PROP_FULLSCREEN) cv2.setWindowProperty("test", cv2.WND_PROP_FULLSCREEN, cv2.cv.CV_WINDOW_FULLSCREEN) cv2.imshow("test", queryImage) cv2.waitKey(1000) # Initiate ORB features detector orb = cv2.ORB(nfeatures=100000) # Find the keypoints and descriptors with ORB kp1, des1 = orb.detectAndCompute(queryImage, None) # Use the FLANN Matcher (Fast Approximate Nearest Neighbor Search Library) flann_params = dict(algorithm=6, # FLANN_INDEX_LSH table_number=6, key_size=12, multi_probe_level=1) flann = cv2.FlannBasedMatcher(flann_params, {}) source = 0 cap = create_capture(args) results = [] img = None for i in range(args.frames): ret, img = cap.read() if ret is False: raise SystemExit( "ERROR: unable to capture from video source: {}".format(source)) trainImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Find the keypoints and descriptors with ORB kp2, des2 = orb.detectAndCompute(trainImage, None) if des2 is None: raise SystemExit( "ERROR: Not enough keypoints in video capture, aborting...") matches = flann.knnMatch(des1, des2, k=2) # store all the good matches as per Lowe's ratio test good_matches = [m[0] for m in matches if len(m) == 2 and m[0].distance < m[1].distance * 0.7] results.append(len(good_matches)) cv2.waitKey(1000) cv2.destroyAllWindows() if args.output: cv2.imwrite(args.output, img) print('Screenshot saved to: {}'.format(args.output)) # Remove Max and Min values from results results.remove(max(results)) results.remove(min(results)) avg = sum(results) / len(results) if avg > args.min_matches: print("Match found! ({} > {})".format(avg, args.min_matches)) else: raise SystemExit( "ERROR: Not enough matches are found - {} < {}".format( avg, args.min_matches)) 2013.com.canonical.certification.checkbox-0.4/bin/lock_screen_watcher0000775000175000017500000000602512320541306025525 0ustar zygazyga00000000000000#!/usr/bin/env python3 import argparse import sys import subprocess from gi.repository import GObject from checkbox_support.dbus import connect_to_system_bus import threading import time GObject.threads_init() class SceenSaverStatusHelper(threading.Thread): def __init__(self, loop): super(SceenSaverStatusHelper, self).__init__() self._loop = loop self.quit = False def query(self): p = subprocess.Popen(["gnome-screensaver-command", "-q"], stdout=subprocess.PIPE) stdout, stderr = p.communicate() # parse the stdout string from the command "gnome-screensaver-command -q" # the result should be "active" or "inactive" if "active" == stdout.decode().split(" ")[-1][0:-1] : print("the screensaver is active") self._loop.quit() def run(self): while not self.quit: GObject.idle_add(self.query) time.sleep(1) class HotkeyFunctionListener: def __init__(self, system_bus, loop): self._bus = system_bus self._loop = loop # Assume the test passes, this is changed when timeout expires self._error = False def _on_timeout_expired(self): """ Internal function called when the timer expires. Basically it's just here to tell the user the test failed or that the user was unable to pressed the hot key during the allowed time. """ print("You have failed to perform the required manipulation in time") # Fail the test when the timeout was reached self._error = True # Stop the loop now self._loop.quit() def check(self, timeout): """ Run the configured test and return the result The result is False if the test has failed. The timeout, when non-zero, will make the test fail after the specified seconds have elapsed without conclusive result. """ # Setup a timeout if requested if timeout > 0: GObject.timeout_add_seconds(timeout, self._on_timeout_expired) # helper to listen the functionality is triggered or not query_thread = SceenSaverStatusHelper(self._loop) query_thread.start() self._loop.run() query_thread.quit = True # Return the outcome of the test return self._error def main(): description = "Wait for the specified hotkey to be pressed." parser = argparse.ArgumentParser(description=description) parser.add_argument('--timeout', type=int, default=30) args = parser.parse_args() # Connect to the system bus, we also get the event # loop as we need it to start listening for signals. system_bus, loop = connect_to_system_bus() listener = HotkeyFunctionListener(system_bus, loop) # Run the actual listener and wait till it either times out or discovers # the specific hot key pressed. try: return listener.check(args.timeout) except KeyboardInterrupt: return 1 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/glob_test0000775000175000017500000001066512320541306023510 0ustar zygazyga00000000000000#!/usr/bin/python from Globs import benchmarks, hwd import argparse import locale import logging import sys import os #Magic to override the _ function from gettext, since #it's overkill for our needs here. def underscore(string, *args, **kwargs): return(string) __builtins__._ = underscore class Application: def __init__(self, share_dir='', bench_dir='', width=800, height=600, time=5, repetitions=1, fullscreen=False, min_fps=60, ignore_problems=False): self.opts= {} self.opts['width'] = width self.opts['height'] = height self.opts['time'] = time self.opts['repetitions'] = repetitions self.opts['fullscreen'] = fullscreen self.min_fps = min_fps self.ignore_problems = ignore_problems self.share_dir = share_dir self.bench_dir = bench_dir def run(self): test_pass = True self.hwd = hwd.HWDetect() self.bm = benchmarks.Benchmarks(os.path.join(self.bench_dir, 'benchmarks')) ver_str = self.hwd.get_gl_info()['version'] hwd_ext = self.hwd.get_gl_info()['extensions'] bench_list = [name for name in self.bm.get_names() if name != 'Fake'] for benchmark_name in bench_list : runnable = True if self.bm.check_ver(benchmark_name, ver_str) == False: logging.warning("%s requires OpenGL version %s, I have %s", benchmark_name, self.bm.get_info(benchmark_name)['glversion'], ver_str) runnable = False test_pass = False ext_list = self.bm.check_ext(benchmark_name, hwd_ext) if ext_list.__class__ == list: # Returned a list of missing exts missing_ext = '' for ext in ext_list: missing_ext += ext if ext_list.index(ext) != len(ext_list) - 1: missing_ext += ', ' logging.warning("Missing extensions: %s",missing_ext) runnable = False test_pass = False if runnable: fps = self.bm.run(benchmark_name, self.opts) if fps is None: #oops, test failed to produce usable result! print("Test failed to produce FPS measurement.") print("Possible causes: OpenGL version too low/high") if self.ignore_problems: print("Ignoring this as requested") else: print("Considering this a FAIL test") test_pass = False else: print("{} {} fps".format(benchmark_name, fps)) if ( self.min_fps > fps): print("(Failed to meet minimum {} FPS)".format( self.min_fps)) test_pass = False return test_pass share_dir = '/usr/share/globs' locale_dir = '/usr/share/locale' bench_dir = '/usr/lib/globs' parser = argparse.ArgumentParser("Executes gl benchmarks non-interactively") parser.add_argument("--width", action='store', default=800, type=int) parser.add_argument("--height", action='store', default=600, type=int) parser.add_argument("--repetitions", action='store', default=1, type=int) parser.add_argument("--time", action='store', default=10, type=int) parser.add_argument("--ignore-problems", action='store_true', default=False, help=("If a test fails to " "produce a FPS rating, ignore it for global test " "outcome purposes")) parser.add_argument("--fullscreen", action='store_true', default=False) parser.add_argument("--min-fps", action='store', default=60.0, type=float, help=("If any of the benchmarks" "obtains less than this FPS, the test will be considered" "failed")) args = parser.parse_args() app = Application(share_dir, bench_dir, args.width, args.height, args.time, args.repetitions, args.fullscreen, args.min_fps, args.ignore_problems) if app.run(): print("PASS") sys.exit(0) else: print("FAIL") sys.exit(1) 2013.com.canonical.certification.checkbox-0.4/bin/check_is_laptop0000775000175000017500000000077412320541306024655 0ustar zygazyga00000000000000#!/bin/bash # Establish the system type based on DMI info TYPE=$(dmidecode -t 3 | awk '/Type:/ { print $2 }') echo "Type: " $TYPE BATTERY="NO" for device in `find /sys -name "type"` do if [ "$(cat $device)" == "Battery" ]; then BATTERY="YES" fi done echo "Battery: " $BATTERY case $TYPE in Notebook|Laptop|Portable) exit 0 ;; *) # Give the system a second chance based on the battery info if [ $BATTERY == "YES" ]; then exit 0 else exit 1 fi ;; esac 2013.com.canonical.certification.checkbox-0.4/bin/sleep_test_log_check0000775000175000017500000001735012320541306025671 0ustar zygazyga00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # This file is part of Checkbox. # # Copyright 2014 Canonical Ltd. # # Authors # Jeff Lane # Daniel Manrique # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . ''' This script is used to parse the log generated by fwts and check it for certain errors detected during testing. It expects that this is a log file created by fwts at runtime using the -l option. It's written now specifically for checking ater the fwts s3 and s4 tests but can be adapted to look for other tests, or all tests. ''' import sys import collections import re from argparse import ArgumentParser import logging # Definitions of when a level starts, how a failure looks, # and when a level ends. start_level_re = r'^(?P.+) failures: (?PNONE|\d+)$' start_level_re = re.compile(start_level_re) failure_re = re.compile(r'^ (?P(s3|s4)): (?P
.+)$') end_level_re = re.compile(r"$^") def parse_summary(summary, results): """ Parses an entire "Test Failure Summary" section, which contains a short summary of failures observed per level. Returns nothing, but adds the results to the passed results dictionary. :param summary: A list of lines comprised in this summary section :param results: The results dictionary into which to put the end result. Should be a dict with keys for each level, the values are dicts with keys for each test (s3, s4) which in turn contain a list of all the failures observed for that level and test. """ current_level = None current_acum = [] for logline in summary: level_matches = start_level_re.search(logline) if level_matches: logging.debug("Found a level: %s", level_matches.group('level')) current_level = level_matches.group('level') elif end_level_re.search(logline) and current_level: if current_level: logging.debug("Current level (%s) has %s", current_level, current_acum) # By passing results[current_level] a key in results will be # created for every level we see, regardless of whether it # reports failures or not. This is OK because we can later # check results' keys to ensure we saw at least one level; if # results has no keys, it could mean a malformed fwts log file. parse_level(current_acum, results[current_level]) else: logging.debug("Discarding junk") current_acum = [] current_level = None else: current_acum.append(logline) def parse_level(level_lines, level_results): """ Parses the level's lines, appending the failures to the level's results. level_results is a dictionary with a key per test type (s3, s4, and so on). Returns nothing, but adds the results to the passed results dictionary for this level. :param level_lines: A list of lines comprised in this level's list of failures. : param level_results: A dictionary containing this level's results. Should be a dict with keys for each test, to which the failures for the level will be appended. """ for failureline in level_lines: failure_matches = failure_re.search(failureline) if failure_matches: test = failure_matches.group('test') details = failure_matches.group('details') logging.debug("fail %s was %s", test, details) level_results[test].append(details) def main(): parser = ArgumentParser() parser.add_argument('-d', '--debug', action='store_const', const=logging.DEBUG, default=logging.INFO, help="Show debugging information.") parser.add_argument('-v', '--verbose', action='store_true', default=False, help="Display each error discovered. May provide \ very long output. Also, this option will only \ provide a list of UNIQUE errors encountered in \ the log file. It will not display duplicates. \ Default is [%(default)s]") parser.add_argument('test', action='store', choices=['s3', 's4'], help='The test to check (s3 or s4)') parser.add_argument('logfile', action='store', help='The log file to parse') args = parser.parse_args() logging.basicConfig(level=args.debug) #Create a generator and get our lines log = (line.rstrip() for line in open(args.logfile, 'rt', encoding="UTF-8")) # End result will be a dictionary with a key per level, value is another # dictionary with a key per test (s3, s4, ...) and a list of all failures # for each test. Duplicates are possible, because we should also indicate # the number of instances for each failure. results = collections.defaultdict(lambda: collections.defaultdict(list)) sum_acum = [] summaries_found = 0 # Start parsing the fwts log. Gather each "Test Failure Summary" section # and when it's complete, pass it to the parse_summary function to extract # levels and tests. for logline in log: if "Test Failure Summary" in logline: parse_summary(sum_acum, results) summaries_found += 1 sum_acum = [] else: sum_acum.append(logline) # We reached the end, so add the last accumulated summary if sum_acum: parse_summary(sum_acum, results) # Report what I found for level in sorted(results.keys()): if results[level]: # Yes, we can have an empty level. We may have # seen the levelheader but had it report no # failures. print("{} failures:".format(level)) for test in results[level].keys(): print(" {}: {} failures".format(test, len(results[level][test]))) if args.verbose: print('='*40) counts = collections.Counter(results[level][test]) for failure in counts: print(" {} (x {})".format(failure, counts[failure])) # Decide on the outcome based on the collected information if not summaries_found: logging.error("No fwts test summaries found, " "possible malformed fwts log file") return_code = 2 elif not results.keys(): # If it has no keys, means we didn't see any # FWTS levels logging.error("None of the summaries contained failure levels, " "possible malformed fwts log file") return_code = 2 elif any(results.values()): # If any of the results' levels has errors return_code = 1 else: print("No errors detected") return_code = 0 return return_code if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/wifi_time2reconnect0000775000175000017500000000233212320541306025455 0ustar zygazyga00000000000000#!/usr/bin/env python3 import os import re import sys import time import subprocess from datetime import datetime IFACE = None TIMEOUT = 30 def main(): """ Check the time needed to reconnect an active WIFI connection """ devices = subprocess.getoutput('nmcli dev') match = re.search('(\w+)\s+802-11-wireless\s+connected', devices) if match: IFACE = match.group(1) else: print("No active wifi connection detected", file=sys.stderr) return 1 dev_status = subprocess.getoutput('nmcli -t -f devices,uuid con status') match = re.search(IFACE+':(.*)', dev_status) uuid = None if match: uuid = match.group(1) else: return 1 subprocess.call( 'nmcli dev disconnect iface %s' %IFACE, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT, shell=True) time.sleep(2) start = datetime.now() subprocess.call( 'nmcli con up uuid %s --timeout %s' %(uuid, TIMEOUT), stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT, shell=True) delta = datetime.now() - start print('%.2f Seconds' %delta.total_seconds()) return 0 if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/connect_wireless0000775000175000017500000000230312320541306025062 0ustar zygazyga00000000000000#!/bin/bash # Any active connections? conn='' active_connection=$(nmcli -f SSID,ACTIVE dev wifi list | grep yes) if [ $? -eq 0 ] then ap=$(echo $active_connection | awk -F\' '{print $2}') conn=$(nmcli -t -f UUID,TYPE,NAME con list | grep wireless | grep -e "$ap$" | awk -F\: '{print $1}') else conn=$(nmcli -t -f UUID,TYPE con list | grep wireless | head -n 1 | awk -F\: '{print $1}') fi #Strip trailing/leading whitespace conn=$(echo $conn |sed 's/^[ \t]*//;s/[ \t]*$//') # Find out if wireless is enabled nmcli nm wifi | grep -q 'enabled' if [ $? -ne 0 ] then # Find out why rfkill list wifi | grep 'Hard blocked' | grep -q yes if [ $? -eq 0 ] then blkmessage='Your wireless may be hardware blocked. You may need to use your wireless key/switch to re-enable it.' echo $blkmessage fi fi # Check if there's a connection already (wireless or otherwise) nmcli dev status | grep -q '\' if [ $? -eq 0 ] then # Disconnect, pause for a short time for iface in `nmcli -f GENERAL dev list | grep 'GENERAL.DEVICE' | awk '{print $2}'` do nmcli dev disconnect iface $iface done sleep 2 fi nmcli con up uuid "$conn" 2013.com.canonical.certification.checkbox-0.4/bin/removable_storage_test0000775000175000017500000006414012320541306026262 0ustar zygazyga00000000000000#!/usr/bin/env python3 import argparse import collections import dbus import hashlib import logging import os import subprocess import sys import tempfile import time from gi.repository import GUdev from checkbox_support.dbus import connect_to_system_bus from checkbox_support.dbus.udisks2 import UDISKS2_BLOCK_INTERFACE from checkbox_support.dbus.udisks2 import UDISKS2_DRIVE_INTERFACE from checkbox_support.dbus.udisks2 import UDISKS2_FILESYSTEM_INTERFACE from checkbox_support.dbus.udisks2 import UDisks2Model, UDisks2Observer from checkbox_support.dbus.udisks2 import is_udisks2_supported from checkbox_support.dbus.udisks2 import lookup_udev_device from checkbox_support.dbus.udisks2 import map_udisks1_connection_bus from checkbox_support.heuristics.udisks2 import is_memory_card from checkbox_support.parsers.udevadm import CARD_READER_RE, GENERIC_RE, FLASH_RE from checkbox_support.udev import get_interconnect_speed from checkbox_support.udev import get_udev_block_devices class ActionTimer(): '''Class to implement a simple timer''' def __enter__(self): self.start = time.time() return self def __exit__(self, *args): self.stop = time.time() self.interval = self.stop - self.start class RandomData(): '''Class to create data files''' def __init__(self, size): self.tfile = tempfile.NamedTemporaryFile(delete=False) self.path = '' self.name = '' self.path, self.name = os.path.split(self.tfile.name) self._write_test_data_file(size) def _generate_test_data(self): seed = "104872948765827105728492766217823438120" phrase = ''' Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. ''' words = phrase.replace('\n', '').split() word_deque = collections.deque(words) seed_deque = collections.deque(seed) while True: yield ' '.join(list(word_deque)) word_deque.rotate(int(seed_deque[0])) seed_deque.rotate(1) def _write_test_data_file(self, size): data = self._generate_test_data() while os.path.getsize(self.tfile.name) < size: self.tfile.write(next(data).encode('UTF-8')) return self def md5_hash_file(path): md5 = hashlib.md5() try: with open(path, 'rb') as stream: while True: data = stream.read(8192) if not data: break md5.update(data) except IOError as exc: logging.error("unable to checksum %s: %s", path, exc) return None else: return md5.hexdigest() class DiskTest(): ''' Class to contain various methods for testing removable disks ''' def __init__(self, device, memorycard): self.rem_disks = {} # mounted before the script running self.rem_disks_nm = {} # not mounted before the script running self.rem_disks_memory_cards = {} self.rem_disks_memory_cards_nm = {} self.rem_disks_speed = {} self.data = '' self.device = device self.memorycard = memorycard self._probe_disks() def read_file(self, source): with open(source, 'rb') as infile: try: self.data = infile.read() except IOError as exc: logging.error("Unable to read data from %s: %s", source, exc) return False else: return True def write_file(self, data, dest): try: outfile = open(dest, 'wb', 0) except OSError as exc: logging.error("Unable to open %s for writing.", dest) logging.error(" %s", exc) return False with outfile: try: outfile.write(self.data) except IOError as exc: logging.error("Unable to write data to %s: %s", dest, exc) return False else: outfile.flush() os.fsync(outfile.fileno()) return True def clean_up(self, target): try: os.unlink(target) except OSError as exc: logging.error("Unable to remove tempfile %s", target) logging.error(" %s", exc) def _probe_disks(self): """ Internal method used to probe for available disks Indirectly sets: self.rem_disks{,_nm,_memory_cards,_memory_cards_nm,_speed} """ bus, loop = connect_to_system_bus() if is_udisks2_supported(bus): self._probe_disks_udisks2(bus) else: self._probe_disks_udisks1(bus) def _probe_disks_udisks2(self, bus): """ Internal method used to probe / discover available disks using udisks2 dbus interface using the provided dbus bus (presumably the system bus) """ # We'll need udisks2 and udev to get the data we need udisks2_observer = UDisks2Observer() udisks2_model = UDisks2Model(udisks2_observer) udisks2_observer.connect_to_bus(bus) udev_client = GUdev.Client() # Get a collection of all udev devices corresponding to block devices udev_devices = get_udev_block_devices(udev_client) # Get a collection of all udisks2 objects udisks2_objects = udisks2_model.managed_objects # Let's get a helper to simplify the loop below def iter_filesystems_on_block_devices(): """ Generate a collection of UDisks2 object paths that have both the filesystem and block device interfaces """ for udisks2_object_path, interfaces in udisks2_objects.items(): if (UDISKS2_FILESYSTEM_INTERFACE in interfaces and UDISKS2_BLOCK_INTERFACE in interfaces): yield udisks2_object_path # We need to know about all IO candidates, # let's iterate over all the block devices reported by udisks2 for udisks2_object_path in iter_filesystems_on_block_devices(): # Get interfaces implemented by this object udisks2_object = udisks2_objects[udisks2_object_path] # Find the path of the udisks2 object that represents the drive # this object is a part of drive_object_path = ( udisks2_object[UDISKS2_BLOCK_INTERFACE]['Drive']) # Lookup the drive object, if any. This can fail when try: drive_object = udisks2_objects[drive_object_path] except KeyError: logging.error( "Unable to locate drive associated with %s", udisks2_object_path) continue else: drive_props = drive_object[UDISKS2_DRIVE_INTERFACE] # Get the connection bus property from the drive interface of the # drive object. This is required to filter out the devices we don't # want to look at now. connection_bus = drive_props["ConnectionBus"] desired_connection_buses = set([ map_udisks1_connection_bus(device) for device in self.device]) # Skip devices that are attached to undesired connection buses if connection_bus not in desired_connection_buses: continue # Lookup the udev object that corresponds to this object try: udev_device = lookup_udev_device(udisks2_object, udev_devices) except LookupError: logging.error( "Unable to locate udev object that corresponds to: %s", udisks2_object_path) continue # Get the block device pathname, # to avoid the confusion, this is something like /dev/sdbX dev_file = udev_device.get_device_file() # Get the list of mount points of this block device mount_points = ( udisks2_object[UDISKS2_FILESYSTEM_INTERFACE]['MountPoints']) # Get the speed of the interconnect that is associated with the # block device we're looking at. This is purely informational but # it is a part of the required API interconnect_speed = get_interconnect_speed(udev_device) if interconnect_speed: self.rem_disks_speed[dev_file] = ( interconnect_speed * 10 ** 6) else: self.rem_disks_speed[dev_file] = None # We need to skip-non memory cards if we look for memory cards and # vice-versa so let's inspect the drive and use heuristics to # detect memory cards (a memory card reader actually) now. if self.memorycard != is_memory_card(drive_props['Vendor'], drive_props['Model'], drive_props['Media']): continue # The if/else test below simply distributes the mount_point to the # appropriate variable, to keep the API requirements. It is # confusing as _memory_cards is variable is somewhat dummy. if mount_points: # XXX: Arbitrarily pick the first of the mount points mount_point = mount_points[0] self.rem_disks_memory_cards[dev_file] = mount_point self.rem_disks[dev_file] = mount_point else: self.rem_disks_memory_cards_nm[dev_file] = None self.rem_disks_nm[dev_file] = None def _probe_disks_udisks1(self, bus): """ Internal method used to probe / discover available disks using udisks1 dbus interface using the provided dbus bus (presumably the system bus) """ ud_manager_obj = bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks') for dev in ud_manager.EnumerateDevices(): device_obj = bus.get_object("org.freedesktop.UDisks", dev) device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) udisks = 'org.freedesktop.UDisks.Device' if not device_props.Get(udisks, "DeviceIsDrive"): dev_bus = device_props.Get(udisks, "DriveConnectionInterface") if dev_bus in self.device: parent_model = parent_vendor = '' if device_props.Get(udisks, "DeviceIsPartition"): parent_obj = bus.get_object( "org.freedesktop.UDisks", device_props.Get(udisks, "PartitionSlave")) parent_props = dbus.Interface( parent_obj, dbus.PROPERTIES_IFACE) parent_model = parent_props.Get(udisks, "DriveModel") parent_vendor = parent_props.Get(udisks, "DriveVendor") parent_media = parent_props.Get(udisks, "DriveMedia") if self.memorycard: if (dev_bus != 'sdio' and not FLASH_RE.search(parent_media) and not CARD_READER_RE.search(parent_model) and not GENERIC_RE.search(parent_vendor)): continue else: if (FLASH_RE.search(parent_media) or CARD_READER_RE.search(parent_model) or GENERIC_RE.search(parent_vendor)): continue dev_file = str(device_props.Get(udisks, "DeviceFile")) dev_speed = str(device_props.Get(udisks, "DriveConnectionSpeed")) self.rem_disks_speed[dev_file] = dev_speed if len(device_props.Get(udisks, "DeviceMountPaths")) > 0: devPath = str(device_props.Get(udisks, "DeviceMountPaths")[0]) self.rem_disks[dev_file] = devPath self.rem_disks_memory_cards[dev_file] = devPath else: self.rem_disks_nm[dev_file] = None self.rem_disks_memory_cards_nm[dev_file] = None def mount(self): passed_mount = {} for key in self.rem_disks_nm: temp_dir = tempfile.mkdtemp() if self._mount(key, temp_dir) != 0: logging.error("can't mount %s", key) else: passed_mount[key] = temp_dir if len(self.rem_disks_nm) == len(passed_mount): self.rem_disks_nm = passed_mount return 0 else: count = len(self.rem_disks_nm) - len(passed_mount) self.rem_disks_nm = passed_mount return count def _mount(self, dev_file, mount_point): return subprocess.call(['mount', dev_file, mount_point]) def umount(self): errors = 0 for disk in self.rem_disks_nm: if not self.rem_disks_nm[disk]: continue if self._umount(disk) != 0: errors += 1 logging.error("can't umount %s on %s", disk, self.rem_disks_nm[disk]) return errors def _umount(self, mount_point): # '-l': lazy umount, dealing problem of unable to umount the device. return subprocess.call(['umount', '-l', mount_point]) def clean_tmp_dir(self): for disk in self.rem_disks_nm: if not self.rem_disks_nm[disk]: continue if not os.path.ismount(self.rem_disks_nm[disk]): os.rmdir(self.rem_disks_nm[disk]) def main(): parser = argparse.ArgumentParser() parser.add_argument('device', choices=['usb', 'firewire', 'sdio', 'scsi', 'ata_serial_esata'], nargs='+', help=("The type of removable media " "(usb, firewire, sdio, scsi or ata_serial_esata)" "to test.")) parser.add_argument('-l', '--list', action='store_true', default=False, help="List the removable devices and mounting status") parser.add_argument('-m', '--min-speed', action='store', default=0, type=int, help="Minimum speed a device must support to be " "considered eligible for being tested (bits/s)") parser.add_argument('-p', '--pass-speed', action='store', default=0, type=int, help="Minimum average throughput from all eligible" "devices for the test to pass (MB/s)") parser.add_argument('-i', '--iterations', action='store', default='1', type=int, help=("The number of test cycles to run. One cycle is" "comprised of generating --count data files of " "--size bytes and writing them to each device.")) parser.add_argument('-c', '--count', action='store', default='1', type=int, help='The number of random data files to generate') parser.add_argument('-s', '--size', action='store', type=int, default=1048576, help=("The size of the test data file to use " "in Bytes. Default is %(default)s")) parser.add_argument('-n', '--skip-not-mount', action='store_true', default=False, help=("skip the removable devices " "which haven't been mounted before the test.")) parser.add_argument('--memorycard', action="store_true", help=("Memory cards devices on bus other than sdio " "require this parameter to identify " "them as such")) args = parser.parse_args() test = DiskTest(args.device, args.memorycard) errors = 0 # If we do have removable drives attached and mounted if len(test.rem_disks) > 0 or len(test.rem_disks_nm) > 0: if args.list: # Simply output a list of drives detected print('-' * 20) print("Removable devices currently mounted:") if args.memorycard: if len(test.rem_disks_memory_cards) > 0: for disk, mnt_point in test.rem_disks_memory_cards.items(): print("%s : %s" % (disk, mnt_point)) else: print("None") print("Removable devices currently not mounted:") if len(test.rem_disks_memory_cards_nm) > 0: for disk in test.rem_disks_memory_cards_nm: print(disk) else: print("None") else: if len(test.rem_disks) > 0: for disk, mnt_point in test.rem_disks.items(): print("%s : %s" % (disk, mnt_point)) else: print("None") print("Removable devices currently not mounted:") if len(test.rem_disks_nm) > 0: for disk in test.rem_disks_nm: print(disk) else: print("None") print('-' * 20) return 0 else: # Create a file, copy to disk and compare hashes if args.skip_not_mount: disks_all = test.rem_disks else: # mount those haven't be mounted yet. errors_mount = test.mount() if errors_mount > 0: print("There're total %d device(s) failed at mounting." % errors_mount) errors += errors_mount disks_all = dict(list(test.rem_disks.items()) + list(test.rem_disks_nm.items())) if len(disks_all) > 0: print("Found the following mounted %s partitions:" % ', '.join(args.device)) for disk, mount_point in disks_all.items(): supported_speed = test.rem_disks_speed[disk] print(" %s : %s : %s bits/s" % (disk, mount_point, supported_speed), end="") if (args.min_speed and int(args.min_speed) > int(supported_speed)): print(" (Will not test it, speed is below %s bits/s)" % args.min_speed, end="") print("") print('-' * 20) disks_eligible = {disk: disks_all[disk] for disk in disks_all if not args.min_speed or int(test.rem_disks_speed[disk]) >= int(args.min_speed)} write_sizes = [] test_files = {} # Generate our data file(s) for count in range(args.count): test_files[count] = RandomData(args.size) write_sizes.append(os.path.getsize( test_files[count].tfile.name)) total_write_size = sum(write_sizes) try: for disk, mount_point in disks_eligible.items(): print("%s (Total Data Size / iteration: %0.4f MB):" % (disk, (total_write_size / 1024 / 1024))) iteration_write_size = ( total_write_size * args.iterations) / 1024 / 1024 iteration_write_times = [] for iteration in range(args.iterations): target_file_list = [] write_times = [] for file_index in range(args.count): parent_file = test_files[file_index].tfile.name parent_hash = md5_hash_file(parent_file) target_filename = ( test_files[file_index].name + '.%s' % iteration) target_path = mount_point target_file = os.path.join(target_path, target_filename) target_file_list.append(target_file) test.read_file(parent_file) with ActionTimer() as timer: if not test.write_file(test.data, target_file): logging.error( "Failed to copy %s to %s", parent_file, target_file) errors += 1 continue write_times.append(timer.interval) child_hash = md5_hash_file(target_file) if parent_hash != child_hash: logging.warning( "[Iteration %s] Parent and Child" " copy hashes mismatch on %s!", iteration, target_file) logging.warning( "\tParent hash: %s", parent_hash) logging.warning( "\tChild hash: %s", child_hash) errors += 1 for file in target_file_list: test.clean_up(file) total_write_time = sum(write_times) avg_write_time = total_write_time / args.count try: avg_write_speed = (( total_write_size / total_write_time) / 1024 / 1024) except ZeroDivisionError: avg_write_speed = 0.00 finally: iteration_write_times.append(total_write_time) print("\t[Iteration %s] Average Speed: %0.4f" % (iteration, avg_write_speed)) for iteration in range(args.iterations): iteration_write_time = sum(iteration_write_times) print("\tSummary:") print("\t\tTotal Data Attempted: %0.4f MB" % iteration_write_size) print("\t\tTotal Time to write: %0.4f secs" % iteration_write_time) print("\t\tAverage Write Time: %0.4f secs" % (iteration_write_time / args.iterations)) try: avg_write_speed = (iteration_write_size / iteration_write_time) except ZeroDivisionError: avg_write_speed = 0.00 finally: print("\t\tAverage Write Speed: %0.4f MB/s" % avg_write_speed) finally: for key in range(args.count): test.clean_up(test_files[key].tfile.name) if (len(test.rem_disks_nm) > 0): if test.umount() != 0: errors += 1 test.clean_tmp_dir() if errors > 0: logging.warning( "Completed %s test iterations, but there were" " errors", args.count) return 1 elif len(disks_eligible) == 0: logging.error( "No %s disks with speed higher than %s bits/s", args.device, args.min_speed) return 1 else: #Pass is not assured! if (not args.pass_speed or avg_write_speed >= args.pass_speed): return 0 else: print("FAIL: Average speed was lower than desired " "pass speed of %s MB/s" % args.pass_speed) return 1 else: logging.error("No device being mounted successfully " "for testing, aborting") return 1 else: # If we don't have removable drives attached and mounted logging.error("No removable drives were detected, aborting") return 1 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/color_depth_info0000775000175000017500000000370512320541306025040 0ustar zygazyga00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # # color_depth_info # # This file is part of Checkbox. # # Copyright 2012 Canonical Ltd. # # Authors: Alberto Milone # # Checkbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # Checkbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Checkbox. If not, see . """ The get_color_depth got information from Xorg.*.log """ import os import re import sys from glob import glob def get_color_depth(log_dir='/var/log/'): '''Return color depth and pixmap format''' # find the most recent X.org log depth = 8 pixmap_format = 8 log = None max_time = 0 for log in glob(os.path.join(log_dir, 'Xorg.*.log')): mtime = os.stat(log).st_mtime if mtime > max_time: max_time = mtime current_log = log if current_log is None: depth = 0 pixmap_format = 0 return (depth, pixmap_format) with open(current_log, 'rb') as stream: for match in re.finditer('Depth (\d+) pixmap format is (\d+) bpp', str(stream.read())): depth = int(match.group(1)) pixmap_format = int(match.group(2)) return (depth, pixmap_format) def main(): '''main function''' depth, pixmap_format = get_color_depth() print('Color Depth: {0}\nPixmap Format: {1} bpp'.format(depth, pixmap_format)) if depth == 8: return 1 return 0 if __name__ == '__main__': sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/spindown0000775000175000017500000000522412320541306023362 0ustar zygazyga00000000000000#!/bin/bash # # Copyright (C) 2012 Canonical # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # get_load_cycle() { smartctl -A /dev/sda | grep Load_Cycle_Count | awk '{print $10}' } get_time_secs() { date "+%s" } if [ $EUID -ne 0 ]; then echo "Need to run as root e.g. use sudo" exit 1 fi hdparm -B 127 /dev/sda TEST_FILE=test MAX_TIMEOUT=60 MAX_ITERATIONS=10 dirty_disk() { rm -f ${TEST_FILE} touch ${TEST_FILE} truncate -s 4K ${TEST_FILE} } drop_caches() { sync (echo 1 | sudo tee /proc/sys/vm/drop_caches) > /dev/null (echo 2 | sudo tee /proc/sys/vm/drop_caches) > /dev/null (echo 3 | sudo tee /proc/sys/vm/drop_caches) > /dev/null } find_load_cycle_threshold() { lc1=0 lc2=0 count=0 TIMEOUT=1 echo Attempting to find Spin Down timeout for this HDD while [ $lc1 -eq $lc2 -a $TIMEOUT -lt $MAX_TIMEOUT ] do lc1=$(get_load_cycle) count=$((count + 1)) dirty_disk drop_caches sleep $TIMEOUT lc2=$(get_load_cycle) n=$((lc2 - lc1)) echo Checking with timeout: $TIMEOUT seconds, Load Cycles: $n if [ $TIMEOUT -lt 15 ]; then TIMEOUT=$((TIMEOUT + 1)) else TIMEOUT=$((TIMEOUT + $TIMEOUT/5)) fi done } exercise_load_cycle() { echo "Attempting to exercise load cycle on HDD" i=0 t1=$(get_time_secs) n1=$(get_load_cycle) # bump timeout by 1 second just to make sure # we can always catch the load cycle window TIMEOUT=$((TIMEOUT + 1)) while [ $i -lt $MAX_ITERATIONS ] do i=$((i + 1)) echo "Load Cycle $i of $MAX_ITERATIONS" dirty_disk drop_caches sleep $TIMEOUT done i=0 t2=$(get_time_secs) n2=$(get_load_cycle) t=$((t2 - t1)) n=$((n2 - n1)) echo "Managed to force $n Load Cycles in $t seconds." life=$((1000000 * $t / $n)) days=$((life / (3600 * 24))) echo "At this rate, the HDD will fail after $days days." } find_load_cycle_threshold if [ $TIMEOUT -lt $MAX_TIMEOUT ]; then echo "HDD seems to be spinning down aggressively." exercise_load_cycle exit 1 else echo "Gave up looking for Load Cycle timeout threshold, HDD looks sane." exit 0 fi 2013.com.canonical.certification.checkbox-0.4/bin/bluetooth_test0000775000175000017500000001257412320541306024573 0ustar zygazyga00000000000000#!/usr/bin/env python3 from subprocess import CalledProcessError, check_output, STDOUT from tempfile import TemporaryDirectory import argparse import logging import os import re import sys import time OBEX_RESPONSE_CODE = { 0x10: "Continue", 0x20: "OK, Success", 0x21: "Created", 0x22: "Accepted", 0x23: "Non-Authoritative Information", 0x24: "No Content", 0x25: "Reset Content", 0x26: "Partial Content", 0x30: "Multiple Choices", 0x31: "Moved Permanently", 0x32: "Moved temporarily", 0x33: "See Other", 0x34: "Not modified", 0x35: "Use Proxy", 0x40: "Bad Request - server couldn't understand request", 0x41: "Unauthorized", 0x42: "Payment required", 0x43: "Forbidden - operation is understood but refused", 0x44: "Not Found", 0x45: "Method not allowed", 0x46: "Not Acceptable", 0x47: "Proxy Authentication required", 0x48: "Request Time Out", 0x49: "Conflict", 0x4A: "Gone", 0x4B: "Length Required", 0x4C: "Precondition failed", 0x4D: "Requested entity too large", 0x4E: "Request URL too large", 0x4F: "Unsupported media type", 0x50: "Internal Server Error", 0x51: "Not Implemented", 0x52: "Bad Gateway", 0x53: "Service Unavailable", 0x54: "Gateway Timeout", 0x55: "HTTP version not supported", 0x60: "Database Full", 0x61: "Database Locked" } class ObexFTPTest: def __init__(self, path, btaddr): self._file = path self._filename = os.path.basename(path) self._filesize = os.path.getsize(path) self._btaddr = btaddr def _error_helper(self, pattern, **extra): # obexftp 0.23 version returns 255 on success, see: # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=549623 if 'exception' in extra: exception = extra.get('exception') if re.search(pattern, exception.output): logging.info("PASS") return 0 else: logging.error(exception.output.strip()) if exception.returncode in OBEX_RESPONSE_CODE: logging.error(OBEX_RESPONSE_CODE.get(exception.returncode)) return exception.returncode elif 'output' in extra: output = extra.get('output') if re.search(pattern, output): logging.info("PASS") return 0 else: logging.error(output) return 1 def _run_command(self, command, expected_pattern, cwd=None): try: output = check_output(command, stderr=STDOUT, universal_newlines=True, cwd=cwd) return self._error_helper(expected_pattern, output=output) except OSError as e: logging.error(e) logging.error("Binary not found, " "maybe obexftp is not installed") except CalledProcessError as e: return self._error_helper(expected_pattern, exception=e) finally: # Let the Bluetooth stack enough time to close the connection # before doing another test time.sleep(5) def send(self): logging.info("[ Send test ]".center(80, '=')) logging.info("Using {} as a test file".format(self._filename)) logging.info("Sending {} to {}".format(self._file, self._btaddr)) return self._run_command(["obexput", "-b", self._btaddr, self._file], "Sending.*?done") def browse(self): logging.info("[ Browse test ]".center(80, '=')) logging.info("Checking {} for {}".format(self._btaddr, self._file)) logging.info("Will check for a filesize of {}".format(self._filesize)) return self._run_command(["obexftp", "-b", self._btaddr, "-l"], '{}.*?size="{}"'.format(self._filename, self._filesize)) def remove(self): logging.info("[ Remove test ]".center(80, '=')) logging.info("Removing {} from {}".format(self._filename, self._btaddr)) return self._run_command( ["obexrm", "-b", self._btaddr, self._filename], "Sending.*?done") def get(self): with TemporaryDirectory() as tmpdirname: logging.info("[ Get test ]".center(80, '=')) logging.info("Getting file {} from {}".format(self._filename, self._btaddr)) # Dont trust "get" returncode, it's always 0... return self._run_command( ["obexget", "-b", self._btaddr, self._filename], "Receiving.*?done", cwd=tmpdirname) def main(): description = "Bluetooth tests using ObexFTP" parser = argparse.ArgumentParser(description=description) parser.add_argument('file', type=argparse.FileType('rb')) parser.add_argument('btaddr', help='bluetooth mac address') parser.add_argument('action', choices=['send', 'browse', 'remove', 'get']) args = parser.parse_args() args.file.close() # Configure logging as requested logging.basicConfig( level=logging.INFO, stream=sys.stdout, format='%(levelname)s: %(message)s') return getattr(ObexFTPTest(args.file.name, args.btaddr), args.action)() if __name__ == "__main__": sys.exit(main()) 2013.com.canonical.certification.checkbox-0.4/bin/network_printer_test0000775000175000017500000000207212320541306026012 0ustar zygazyga00000000000000#!/bin/bash usage() { cat < ] [ -s ] -p -- specify a printer to use, by name -s -- specify a network server to use Note: this script expects printers over the IPP protocol only. EOU } while [ $# -gt 0 ] do case "$1" in -p) if echo ${2} | grep -q -c '^-'; then usage exit 1 fi printer=${2} shift ;; -s) if echo ${2} | grep -q -c '^-'; then usage exit 1 fi server=${2} shift ;; --usage) usage exit 1 ;; esac shift done if [ -z $server ]; then echo "Nothing to do with no server defined. (See $0 --usage)" exit 0 fi printer=${printer:-PDF} lpadmin -E -v ipp://${server}/printers/${printer} cupsenable ${printer} cupsaccept ${printer} lsb_release -a | lp -t "lsb_release" -d ${printer} 2013.com.canonical.certification.checkbox-0.4/bin/create_connection0000775000175000017500000002133212320541306025201 0ustar zygazyga00000000000000#!/usr/bin/env python3 import sys import os import time from subprocess import check_call, check_output, CalledProcessError from uuid import uuid4 from argparse import ArgumentParser CONNECTIONS_PATH = '/etc/NetworkManager/system-connections/' def wifi_connection_section(ssid, uuid): if not uuid: uuid = uuid4() connection = """ [connection] id=%s uuid=%s type=802-11-wireless """ % (ssid, uuid) wireless = """ [802-11-wireless] ssid=%s mode=infrastructure""" % (ssid) return connection + wireless def wifi_security_section(security, key): # Add security field to 802-11-wireless section wireless_security = """ security=802-11-wireless-security [802-11-wireless-security] """ if security.lower() == 'wpa': wireless_security += """ key-mgmt=wpa-psk auth-alg=open psk=%s """ % key elif security.lower() == 'wep': wireless_security += """ key-mgmt=none wep-key=%s """ % key return wireless_security def wifi_ip_sections(): ip = """ [ipv4] method=auto [ipv6] method=auto """ return ip def mobilebroadband_connection_section(name, uuid, connection_type): if not uuid: uuid = uuid4() connection_section = """ [connection] id={name} uuid={uuid} type={type} autoconnect=false """.format(name=name, uuid=uuid, type=connection_type) return connection_section def mobilebroadband_type_section(connection_type, apn, username, password, pin): number = ('*99#' if connection_type == 'gsm' else '#777') type_section = """ [{type}] number={number} """.format(type=connection_type, number=number) if apn: type_section += "\napn={apn}".format(apn=apn) if username: type_section += "\nusername={username}".format(username=username) if password: type_section += "\npassword={password}".format(password=password) if pin: type_section += "\npin={pin}".format(pin=pin) return type_section def mobilebroadband_ppp_section(): return """ [ppp] lcp-echo-interval=4 lcp-echo-failure=30 """ def mobilebroadband_ip_section(): return """ [ipv4] method=auto """ def mobilebroadband_serial_section(): return """ [serial] baud=115200 """ def block_until_created(connection, retries, interval): while retries > 0: nmcli_con_list = check_output(['nmcli', 'con', 'list'], universal_newlines=True) if connection in nmcli_con_list: print("Connection %s registered" % connection) break time.sleep(interval) retries = retries - 1 if retries <= 0: print("Failed to register %s." % connection, file=sys.stderr) sys.exit(1) else: try: nmcli_con_up = check_call(['nmcli', 'con', 'up', 'id', connection]) print("Connection %s activated." % connection) except CalledProcessError as error: print("Failed to activate %s." % connection, file=sys.stderr) sys.exit(error.returncode) def write_connection_file(name, connection_info): try: connection_file = open(CONNECTIONS_PATH + name, 'w') connection_file.write(connection_info) os.fchmod(connection_file.fileno(), 0o600) connection_file.close() except IOError: print("Can't write to " + CONNECTIONS_PATH + name + ". Is this command being run as root?", file=sys.stderr) sys.exit(1) def create_wifi_connection(args): wifi_connection = wifi_connection_section(args.ssid, args.uuid) if args.security: # Set security options if not args.key: print("You need to specify a key using --key " "if using wireless security.", file=sys.stderr) sys.exit(1) wifi_connection += wifi_security_section(args.security, args.key) elif args.key: print("You specified an encryption key " "but did not give a security type " "using --security.", file=sys.stderr) sys.exit(1) try: check_call(['rfkill', 'unblock', 'wlan', 'wifi']) except CalledProcessError: print("Could not unblock wireless " "devices with rfkill.", file=sys.stderr) # Don't fail the script if unblock didn't work though wifi_connection += wifi_ip_sections() # NetworkManager replaces forward-slashes in SSIDs with asterisks name = args.ssid.replace('/', '*') write_connection_file(name, wifi_connection) return name def create_mobilebroadband_connection(args): name = args.name mobilebroadband_connection = mobilebroadband_connection_section(name, args.uuid, args.type) mobilebroadband_connection += mobilebroadband_type_section(args.type, args.apn, args.username, args.password, args.pin) if args.type == 'cdma': mobilebroadband_connection += mobilebroadband_ppp_section() mobilebroadband_connection += mobilebroadband_ip_section() mobilebroadband_connection += mobilebroadband_serial_section() write_connection_file(name, mobilebroadband_connection) return name def main(): parser = ArgumentParser() subparsers = parser.add_subparsers(help="sub command help") wifi_parser = subparsers.add_parser('wifi', help='Create a Wifi connection.') wifi_parser.add_argument('ssid', help="The SSID to connect to.") wifi_parser.add_argument('-S', '--security', choices=['wpa', 'wep'], help=("The type of security to be used by the " "connection. No security will be used if " "nothing is specified.")) wifi_parser.add_argument('-K', '--key', help="The encryption key required by the router.") wifi_parser.set_defaults(func=create_wifi_connection) mobilebroadband_parser = subparsers.add_parser('mobilebroadband', help="Create a " "mobile " "broadband " "connection.") mobilebroadband_parser.add_argument('type', choices=['gsm', 'cdma'], help="The type of connection.") mobilebroadband_parser.add_argument('-n', '--name', default='MobileBB', help="The name of the connection.") mobilebroadband_parser.add_argument('-a', '--apn', help="The APN to connect to.") mobilebroadband_parser.add_argument('-u', '--username', help="The username required by the " "mobile broadband access point.") mobilebroadband_parser.add_argument('-p', '--password', help="The password required by the " "mobile broadband access point.") mobilebroadband_parser.add_argument('-P', '--pin', help="The PIN of the SIM " "card, if set.") mobilebroadband_parser.set_defaults(func=create_mobilebroadband_connection) parser.add_argument('-U', '--uuid', help="""The uuid to assign to the connection for use by NetworkManager. One will be generated if not specified here.""") parser.add_argument('-R', '--retries', help="""The number of times to attempt bringing up the connection until it is confirmed as active.""", default=5) parser.add_argument('-I', '--interval', help=("The time to wait between attempts to detect " "the registration of the connection."), default=2) args = parser.parse_args() # Call function to create the appropriate connection type connection_name = args.func(args) # Make sure we don't exit until the connection is fully created block_until_created(connection_name, args.retries, args.interval) if __name__ == "__main__": main() 2013.com.canonical.certification.checkbox-0.4/bin/disk_read_performance_test0000775000175000017500000000474112320541306027071 0ustar zygazyga00000000000000#!/bin/bash # # Verify that disk storage performs at or above baseline performance # #Default to a lower bound of 15 MB/s DEFAULT_BUF_READ=15 for disk in $@; do echo "Beginning $0 test for $disk" echo "---------------------------------------------------" disk_type=`udevadm info --name /dev/$disk --query property | grep "ID_BUS" | awk '{gsub(/ID_BUS=/," ")}{printf $1}'` dev_path=`udevadm info --name /dev/$disk --query property | grep "DEVPATH" | awk '{gsub(/DEVPATH=/," ")}{printf $1}'` echo "INFO: $disk type is $disk_type" case $disk_type in "usb" ) #Custom metrics are guesstimates for now... MIN_BUF_READ=7 # Increase MIN_BUF_READ if a USB3 device is plugged in a USB3 hub port if [[ $dev_path =~ ((.*usb[0-9]+).*\/)[0-9]-[0-9\.:\-]+\/.* ]]; then device_version=`cat '/sys/'${BASH_REMATCH[1]}'/version'` hub_port_version=`cat '/sys/'${BASH_REMATCH[2]}'/version'` if [ $(echo "$device_version >= 3.00"|bc -l) -eq 1 -a $(echo "$hub_port_version >= 3.00"|bc -l) -eq 1 ]; then MIN_BUF_READ=80 fi fi ;; "ide" ) MIN_BUF_READ=40;; * ) MIN_BUF_READ=$DEFAULT_BUF_READ;; esac echo "INFO: $disk_type: Using $MIN_BUF_READ MB/sec as the minimum throughput speed" max_speed=0 echo "" echo "Beginning hdparm timing runs" echo "---------------------------------------------------" for iteration in `seq 1 10`; do speed=`hdparm -t /dev/$disk 2>/dev/null | grep "Timing buffered disk reads" | awk -F"=" '{print $2}' | awk '{print $1}'` echo "INFO: Iteration $iteration: Detected speed is $speed MB/sec" if [ -z "$speed" ]; then echo "WARNING: Device $disk is too small! Aborting test." exit 0 fi speed=${speed/.*} if [ $speed -gt $max_speed ]; then max_speed=$speed fi done echo "INFO: Maximum detected speed is $max_speed MB/sec" echo "---------------------------------------------------" echo "" result=0 if [ $max_speed -gt $MIN_BUF_READ ]; then echo "PASS: $disk Max Speed of $max_speed MB/sec is faster than Minimum Buffer Read Speed of $MIN_BUF_READ MB/sec" else echo "FAIL: $disk Max Speed of $max_speed MB/sec is slower than Minimum Buffer Read Speed of $MIN_BUF_READ MB/sec" result=1 fi done if [ $result -gt 0 ]; then echo "WARNING: One or more disks failed testing!" exit 1 else echo "All devices passed testing!" exit 0 fi 2013.com.canonical.certification.checkbox-0.4/bin/gateway_ping_test0000775000175000017500000002307212320541306025237 0ustar zygazyga00000000000000#!/usr/bin/python3 import os import re import sys import logging import socket import struct import subprocess import gettext import time from gettext import gettext as _ from argparse import ArgumentParser class Route(object): """Gets routing information from the system. """ # auxiliary functions def _hex_to_dec(self, string): """Returns the integer value of a hexadecimal string s """ return int(string, 16) def _num_to_dotted_quad(self, number): """Convert long int to dotted quad string """ return socket.inet_ntoa(struct.pack("\w+)\s+00000000\s+" "(?P[\w]+)\s+") w = h.search(route) if w: if w.group("def_gateway"): return (self._num_to_dotted_quad( self._hex_to_dec(w.group("def_gateway")))) else: logging.error("Could not find def gateway info in /proc") return None else: logging.error("Could not find def gateway info in /proc") return None def _get_default_gateway_from_bin_route(self): """Get default gateway from /sbin/route -n Called by get_default_gateway and is only used if could not get that from /proc """ logging.debug("Reading default gateway information from route binary") routebin = subprocess.getstatusoutput("export LANGUAGE=C; " "/usr/bin/env route -n") if routebin[0] == 0: h = re.compile("\n0.0.0.0\s+(?P[\w.]+)\s+") w = h.search(routebin[1]) if w: def_gateway = w.group("def_gateway") if def_gateway: return def_gateway logging.error("Could not find default gateway by running route") return None def get_hostname(self): return socket.gethostname() def get_default_gateway(self): t1 = self._get_default_gateway_from_proc() if not t1: t1 = self._get_default_gateway_from_bin_route() return t1 def get_host_to_ping(interface=None, verbose=False, default=None): #Get list of all IPs from all my interfaces, interface_list = subprocess.check_output(["ip", "-o", 'addr', 'show']) reg = re.compile('\d: (?P\w+) +inet (?P
[\d\.]+)/' '(?P[\d]+) brd (?P[\d\.]+)') # Will magically exclude lo because it lacks brd field interfaces = reg.findall(interface_list.decode()) # ping -b the network on each one (one ping only) # exclude the ones not specified in iface for iface in interfaces: if not interface or iface[0] == interface: #Use check_output even if I'll discard the output #looks cleaner than using .call and redirecting stdout to null try: (subprocess .check_output(["ping", "-q", "-c", "1", "-b", iface[3]], stderr=subprocess.STDOUT)) except subprocess.CalledProcessError: pass # If default host given, ping it as well, # to try to get it into the arp table. # Needed in case it's not responding to broadcasts. if default: try: subprocess.check_output(["ping", "-q", "-c", "1", default], stderr=subprocess.STDOUT) except subprocess.CalledProcessError: pass ARP_POPULATE_TRIES = 10 num_tries = 0 while num_tries < ARP_POPULATE_TRIES: #Get output from arp -a -n to get known IPs known_ips = subprocess.check_output(["arp", "-a", "-n"]) reg = re.compile('\? \((?P[\d.]+)\) at (?P[a-f0-9\:]+) ' '\[ether\] on (?P[\w\d]+)') #Filter (if needed) IPs not on the specified interface pingable_ips = [pingable[0] for pingable in reg.findall( known_ips.decode()) if not interface or pingable[2] == interface] # If the default given ip is among the remaining ones, # ping that. if default and default in pingable_ips: if verbose: print("Desired ip address %s is reachable, using it" % default) return default #If not, choose another IP. address_to_ping = pingable_ips[0] if len(pingable_ips) else None if verbose: print("Desired ip address %s is not reachable from %s. " % (default, interface)) print("using %s instead." % address_to_ping) if address_to_ping: return address_to_ping time.sleep(2) num_tries += 1 # Wait time expired return None def ping(host, interface, count, deadline, verbose=False): command = "ping -c %s -w %s %s" % (count, deadline, host) if interface: command = ("ping -I%s -c %s -w %s %s" % (interface, count, deadline, host)) reg = re.compile(r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss") ping_summary = None output = os.popen(command) for line in output.readlines(): if verbose: print(line.rstrip()) received = re.findall(reg, line) if received: ping_summary = received[0] ping_summary={'transmitted': int(ping_summary[0]), 'received': int(ping_summary[1]), 'pct_loss': int(ping_summary[2])} return ping_summary def main(args): gettext.textdomain("checkbox") default_count = 2 default_delay = 4 route = Route() parser = ArgumentParser() parser.add_argument("host", nargs='?', default=route.get_default_gateway(), help="Host to ping") parser.add_argument("-c", "--count", default=default_count, type=int, help="Number of packets to send.") parser.add_argument("-d", "--deadline", default=default_delay, type=int, help="Timeouts in seconds.") parser.add_argument("-t", "--threshold", default=0, type=int, help="Percentage of allowed packet loss before " "considering test failed. Defaults to 0 " "(meaning any packet loss will fail the test)") parser.add_argument("-v", "--verbose", action='store_true', help="Be verbose.") parser.add_argument("-I", "--interface", help="Interface to ping from.") args = parser.parse_args() #Ensure count and deadline make sense. Adjust them if not. if args.deadline != default_delay and args.count != default_count: #Ensure they're both consistent, and exit with a warning if #not, rather than modifying what the user explicitly set. if args.deadline <= args.count: print("ERROR: not enough time for %s pings in %s seconds" % (args.count, args.deadline)) return(1) elif args.deadline != default_delay: #Adjust count according to delay. args.count = args.deadline - 1 if args.count < 1: args.count = 1 if args.verbose: print("Adjusting ping count to %s to fit in %s-second deadline" % (args.count, args.deadline)) else: #Adjust delay according to count args.deadline = args.count + 1 if args.verbose: print("Adjusting deadline to %s seconds to fit %s pings" % (args.deadline, args.count)) #If given host is not pingable, override with something pingable. host = get_host_to_ping(interface=args.interface, verbose=args.verbose, default=args.host) if args.verbose: print("Checking connectivity to %s" % host) ping_summary = None if host: ping_summary = ping(host, args.interface, args.count, args.deadline, args.verbose) if ping_summary == None or ping_summary['received'] == 0: print(_("No Internet connection")) return 1 elif ping_summary['transmitted'] != ping_summary['received']: print(_("Connection established, but lost {}% of packets".format( ping_summary['pct_loss']))) if ping_summary['pct_loss'] > args.threshold: print(_("FAIL: {}% packet loss is higher" "than {}% threshold").format(ping_summary['pct_loss'], args.threshold)) return 1 else: print(_("PASS: {}% packet loss is within {}% threshold").format( ping_summary['pct_loss'], args.threshold)) return 0 else: print(_("Internet connection fully established")) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 2013.com.canonical.certification.checkbox-0.4/src/0000775000175000017500000000000012320565735021623 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/src/threaded_memtest.c0000664000175000017500000003461612320565735025317 0ustar zygazyga00000000000000/* $Id: threaded_memtest.c,v 1.7 2008/02/12 01:17:07 gnichols Exp $ * * A scalable, threaded memory exerciser/tester. * * Author: Will Woods * Copyright (C) 2006 Red Hat, Inc. All Rights Reserved. * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Notes: * This program uses sched_setaffinity(), which is Linux-specific. This could * probably be ported to other systems with a fairly simple #ifdef / #define * of setaffinity(), below. You might also have to find a replacement for * sysconf(), which (while a POSIX function) is not available on some other * systems (e.g. OSX). */ #include #include #include #include #include #ifdef __linux__ #include #include #include #include #define __USE_GNU 1 #include #include #ifdef OLD_SCHED_SETAFFINITY #define setaffinity(mask) sched_setaffinity(0,&mask) #else #define setaffinity(mask) sched_setaffinity(0,sizeof(mask),&mask) #endif #define VERSION "$Revision: 1.7 $" /* CVS version info */ #define DEFAULT_THREADS 2 #define DEFAULT_RUNTIME 60*15 #define DEFAULT_MEMPCT 0.95 #define BARLEN 40 /* configurable values used by the threads */ int verbose = 0; int quiet = 0; int parallel = 0; unsigned num_threads, default_threads = DEFAULT_THREADS; unsigned runtime, default_runtime = DEFAULT_RUNTIME; unsigned long memsize, default_memsize; /* system info */ unsigned num_cpus; unsigned long total_ram; /* statistic gathering */ struct timeval start={0,0}, finish={0,0}, duration={0,0}; unsigned long *loop_counters = NULL; /* pointers for threads and their memory regions */ pthread_t *threads; char **mmap_regions = NULL; /* Thread mutexes and conditions */ unsigned created_threads = 0; pthread_mutex_t ct_mutex = PTHREAD_MUTEX_INITIALIZER; unsigned live_threads = 0; pthread_mutex_t lt_mutex = PTHREAD_MUTEX_INITIALIZER; unsigned mmap_done = 0; pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t init_cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mmap_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t mmap_cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t test_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t test_start = PTHREAD_COND_INITIALIZER; pthread_mutex_t finish_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t finish_cond = PTHREAD_COND_INITIALIZER; unsigned done = 0; unsigned running_threads = 0; /* short name of the program */ char *basename = NULL; /* set the affinity for the current task to the given CPU */ int on_cpu(unsigned cpu){ cpu_set_t mask; CPU_ZERO(&mask); CPU_SET(cpu,&mask); if (setaffinity(mask) < 0){ perror("sched_setaffinity"); return -1; } return 0; } /* Parse a memsize string like '34m' or '128k' into a long int */ long unsigned parse_memsize(const char *str) { long unsigned size; char okchars[] = "GgMmKk%"; char unit; size=atoi(str); /* ignores trailing non-digit chars */ unit=str[strlen(str)-1]; if (index(okchars,unit)) { switch (unit) { case 'G': case 'g':size *= 1024; case 'M': case 'm':size *= 1024; case 'K': case 'k':size *= 1024; break; case '%':size = (size/100.0)*total_ram; break; } } return size; } char memsize_str[22]; /* a 64-bit int is 20 digits long */ /* print a nice human-readable string for a large number of bytes */ char *human_memsize(long unsigned size) { char unit=' '; if (size > 10240) { unit='K'; size /= 1024; } if (size > 10240) { unit='M'; size /= 1024; } if (size > 10240) { unit='G'; size /= 1024; } snprintf(memsize_str,22,"%ld%c",size,unit); return memsize_str; } /* A cute little progress bar */ void progressbar(char *label, unsigned cur, unsigned total) { unsigned pos; char bar[BARLEN+1],spinner[]="-\\|/"; pos=(BARLEN*cur)/total; memset(bar,'.',BARLEN); memset(bar,'#',pos); bar[BARLEN]='\0'; if ((pos < BARLEN) && (total >= BARLEN*2)) bar[pos]=spinner[cur%4]; printf("\r%18s [%s] %u/%u",label,bar,cur,total); fflush(stdout); } /* This is the function that the threads run */ void *mem_twiddler(void *arg) { unsigned long thread_id, pages, pagesize, i, p; volatile long garbage; long *lp; int t,offset; char *my_region; unsigned long mapsize = *(unsigned long *)arg; /* Make sure each thread gets a unique ID */ pthread_mutex_lock(&ct_mutex); thread_id=created_threads++; pthread_mutex_unlock(&ct_mutex); if (parallel) { /* let main() go as soon as the thread is created */ mmap_done=1; pthread_cond_signal(&mmap_cond); } on_cpu(thread_id % num_cpus); pagesize=getpagesize(); pages=mapsize/pagesize; /* Map a chunk of memory */ if (verbose) printf("thread %ld: mapping %s RAM\n", thread_id,human_memsize(mapsize)); my_region=mmap(NULL,mapsize,PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE,-1,0); if (my_region == MAP_FAILED) { perror("mmap"); exit(1); } mmap_regions[thread_id] = my_region; /* Dirty each page of the mem region to fault them into existence */ for (i=0;i default_threads) default_threads = num_cpus*2; /* Get memory info */ if (sysinfo(&info) != 0) { perror("sysinfo"); return -1; } free_mem=(info.freeram+info.bufferram)*info.mem_unit; total_ram=info.totalram*info.mem_unit; /* default to using most of free_mem */ default_memsize = free_mem * DEFAULT_MEMPCT; /* Set configurable values to reasonable defaults */ runtime = default_runtime; num_threads = default_threads; memsize = default_memsize; /* parse options */ while ((i = getopt(argc,argv,"hvqpt:n:m:")) != -1) { switch (i) { case 'h': usage(); return 0; case 'v': verbose=1; break; case 'q': quiet=1; break; case 'p': parallel=1; break; case 't': runtime=atoi(optarg); if (!runtime) { printf("%s: error: bad runtime \"%s\"\n",basename,optarg); return 1; } break; case 'n': num_threads=atoi(optarg); if (!num_threads) { printf("%s: error: bad thread count \"%s\"\n",basename,optarg); return 1; } break; case 'm': memsize=parse_memsize(optarg); if (!memsize) { printf("%s: error: bad memory size \"%s\"\n",basename,optarg); return 1; } break; } } /* calculate mapsize now that memsize/num_threads is set */ mapsize = memsize/num_threads; /* sanity checks */ if (num_threads < num_cpus) printf("Warning: num_threads < num_cpus. This isn't usually a good idea.\n"); if (memsize > free_mem) printf("Warning: memsize > free_mem. You will probably hit swap.\n"); /* A little information */ if (verbose) { printf("Detected %u processors.\n",num_cpus); printf("RAM: %.1f%% free (%s/", 100.0*(double)free_mem/(double)total_ram, human_memsize(free_mem)); printf("%s)\n",human_memsize(total_ram)); } printf("Testing %s RAM for %u seconds using %u threads:\n", human_memsize(memsize),runtime,num_threads); /* Allocate room for thread info */ threads=(pthread_t *)malloc(num_threads*sizeof(pthread_t)); mmap_regions=(char **)malloc(num_threads*sizeof(char *)); loop_counters=(unsigned long *)malloc(num_threads*sizeof(unsigned long *)); /* Create all our threads! */ while (created_threads < num_threads) { pthread_mutex_lock(&mmap_mutex); mmap_done=0; if (pthread_create(&threads[created_threads],NULL, mem_twiddler,(void*)&mapsize) != 0) { perror("pthread_create"); exit(1); } /* Wait for it to finish initializing */ while (!mmap_done) { pthread_cond_wait(&mmap_cond,&mmap_mutex); } pthread_mutex_unlock(&mmap_mutex); if (!verbose && !quiet) progressbar("Starting threads",created_threads,num_threads); } if (parallel) { /* Wait for the signal that everyone is finished initializing */ pthread_mutex_lock(&init_mutex); while (live_threads < num_threads) { pthread_cond_wait(&init_cond,&init_mutex); } pthread_mutex_unlock(&init_mutex); } /* Let the testing begin! */ if (!verbose && !quiet) printf("\n"); gettimeofday(&start,NULL); pthread_cond_broadcast(&test_start); /* catch ^C signal */ mysig.sa_handler=int_handler; sigemptyset(&mysig.sa_mask); mysig.sa_flags=0; sigaction(SIGINT,&mysig,NULL); /* Wait for the allotted time */ i=0; while (!done && (i #include #include #include #include #define __USE_GNU 1 #include #define NSEC_PER_SEC 1000000000 #define MAX_JITTER (double)0.2 #define ITERATIONS 10000 #define NSEC(ts) (ts.tv_sec*NSEC_PER_SEC + ts.tv_nsec) #ifdef OLD_SCHED_SETAFFINITY #define setaffinity(mask) sched_setaffinity(0,&mask) #else #define setaffinity(mask) sched_setaffinity(0,sizeof(mask),&mask) #endif int test_clock_jitter(){ cpu_set_t cpumask; struct timespec *time; unsigned long nsec; unsigned slow_cpu, fast_cpu; double jitter; double largest_jitter = 0.0; unsigned cpu, num_cpus, iter; int failures = 0; num_cpus = sysconf(_SC_NPROCESSORS_CONF); if (num_cpus == 1) { printf("Single CPU detected. No clock jitter testing necessary.\n"); return 0; } printf ("Testing for clock jitter on %u cpus\n", num_cpus); time=malloc(num_cpus * sizeof(struct timespec)); for (iter=0; iter NSEC(time[fast_cpu])) { fast_cpu = cpu; } } jitter = ((double)(NSEC(time[fast_cpu]) - NSEC(time[slow_cpu])) / (double)NSEC_PER_SEC); #ifdef DEBUG printf("DEBUG: max jitter for pass %u was %f (cpu %u,%u)\n", iter,jitter,slow_cpu,fast_cpu); #endif if (jitter > MAX_JITTER || jitter < -MAX_JITTER){ printf ("ERROR, jitter = %f\n",jitter); printf ("iter = %u, cpus = %u,%u\n",iter,slow_cpu,fast_cpu); failures++; } if (jitter > largest_jitter) largest_jitter = jitter; } if (failures == 0) printf ("PASSED, largest jitter seen was %lf\n",largest_jitter); else printf ("FAILED, %u iterations failed\n",failures); return (failures > 0); } int test_clock_direction() { time_t starttime = 0; time_t stoptime = 0; int sleeptime = 60; int delta = 0; time(&starttime); sleep(sleeptime); time(&stoptime); delta = (int)stoptime - (int)starttime - sleeptime; printf("clock direction test: start time %d, stop time %d, sleeptime %u, delta %u\n", (int)starttime, (int)stoptime, sleeptime, delta); if (delta != 0) { printf("FAILED\n"); return 1; } /* otherwise */ printf("PASSED\n"); return 0; } int main() { int failures = test_clock_jitter(); if (failures == 0) { failures = test_clock_direction(); } return failures; } 2013.com.canonical.certification.checkbox-0.4/src/Makefile0000664000175000017500000000032012320565735023256 0ustar zygazyga00000000000000.PHONY: all: clocktest threaded_memtest .PHONY: clean clean: rm -f clocktest threaded_memtest threaded_memtest: CFLAGS += -pthread threaded_memtest: CFLAGS += -Wno-unused-but-set-variable CFLAGS += -Wall 2013.com.canonical.certification.checkbox-0.4/src/EXECUTABLES0000664000175000017500000000003312320565735023306 0ustar zygazyga00000000000000clocktest threaded_memtest 2013.com.canonical.certification.checkbox-0.4/data/0000775000175000017500000000000012320567463021745 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/settings/0000775000175000017500000000000012320541307023572 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/settings/shutter.xml0000664000175000017500000001657112320541306026023 0ustar zygazyga00000000000000 2013.com.canonical.certification.checkbox-0.4/data/images/0000775000175000017500000000000012320541307023177 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/images/PNG_Color_Image_Ubuntu.png0000664000175000017500000002737112320541306030144 0ustar zygazyga00000000000000PNG  IHDRXʑ'bKGD pHYsHHFk> vpAgXR(6-IDATxwx?3[%˒ܻ06` &8@HH!В7܄tR.7BHBBp$ \0qjWZkyUvf|gi}99J.ST[ 0 oo!l(xEÊOUZ`.0bo@ Xv[ ОrF/r`,0ӑF3Wef[>pEQEqUN'srksV㏛*w#""T`e:-LX(JZe"F<^` 8pqR=CU~W(- @܂lD23t8W($ǁw`(L࿁{kI*bEQE)hlR\܆Xip? Yj=QEQ%b@n.GVEہoV6B.C`)(Vlqwב{*IH*;F,X( [#.԰,bw`;_ ,X([Ze3 eC)HlpE`)(9)E\mCd V D_7 Xg?Bk; ٫"iXQ*&K((b )._ UR:;J )ذ6?~25v +`)(Qe(-4ʁ gE%G_7`IO#0/݂a`: BdBWm'l(bTl9p#T`)( KqkgRl{0ϕQRFɬS)^p6ΤhB̒2 *Uz`Yt_z#޹+f(KWA@ H?9X(#L$4`.0uH\R^Ks,&LOs LE)_n%}wXwGG8xaED+ ,EQj9n TUt} ÷d2)X 壈LoA"v(*u5M?"w+o$0yk`mi_ӵqq%ö`y p!X*EQ b$FcxC}%=,hbF_yeOWYLg};h{WwnW6'rx/>H!' Ux5;v4EQ@DVYD)h|vG3÷>Wrd6B2DX(˰֛+Sl, l/]*'}K?ZO"/Z1h^!ط$MYVI;szȁ]Xc`z BIzOÊ5REQ1p Ϥ暯t1/*uu'a =Dˉ6$a kEY_3xAN,:z!òbGc*ٽa" `)(Z`3qSlj8EQ2ZS+Zb-X1ۂe Χìd2/,Mn K/n>*P:4(()cOEV:Qyѵl%e=Z8іFh+Ӕ hbha:V8Mw}A^Z*GQEQGbbs.X6O?]*+ hk#m÷k/ @ws/{rphTEQeD3Xk ¬_=fmXMog?z#Ύ0!Kh񍃊z^;D@WREQ cN>hŢ?ⓖ hپ#? ؃č޺;AfPr@⻜q20F(()a_MBeaS< QImk_ѳu6 ζoBIgapdPKQEQTS2V^g,5)Yp^OM7D#tl 8td?Q((ޑBPg%ʀ:aϏ: ,EQEQԼ{`V8e$}8~@ˢxޙm`KeAs|mXT((JĀV`1=[T,߀.5֯%"4M oDMY> L>1ˁI3^Zu((${I]lVC9a}0LO96)a>-KӲf5cX-XJ"+(Ch;9/zJN=ڎнn]k'A͵R<{ѱ4M,No9[٬7 >P5"J%5EU؅Tzǝ@1TSxJR q|+_(Ѝ>z8dCQ%U ڸGCSĺ:!JN^Jٙ+0>|"ŧK׳5UCY_3>kю6)4uz T`>>t)9xDa2 Vr8 VDt)J:D(T5 T~FA&~m/-C%R AahMx0M͇ FQI},z9bm#b&Q~%E%Inxha7RIw?إ/ d`u2/>=X N@@\%lAD:=L|75ƫEʁN}xiYIsv7 h$ }%ډL(^Cn0,\LX-9~v0g}oEL`!G`!5"&PcqehLz{ʤ 䤳([vgla/륕D9 l_9<#McޝE8\]GM>P.CV}v)XXXrѵR7CH{Od"snߓKkb*[!?0.C,ZAny."#b ?,P9|9T?XX# +jxb {3{)ǽ,kHJqr\YR][_vۀf?0:!HT.dUh?% x+2Xs ޴v 3Z:8t$n*Gp)N ="ca b}!~LB_iRbOM`#a VQqeI㜂gR}~l^dYP4g15)k~z|ͻ9 wܨWyQ5qlvj7p }7\SCQe j[hA.=m˹㱲Nnί 0B]tQVXG+-GK`T.o(*ut~u`d߂(H;SP3*F޽_D*!|Rޗ)Yj:`h\ʗt"/XW*@rf}w%nFʛ(JA`ȪBwѦwnX _XG62WSweT]Q~籯 '\Ѹ{T`D"DnA: H0n;>/gu1Ȇi>oH[Oz m |OF[¢=! ?Iӝ q56pN!Iw,E=4l g+2pyX>gv?'+PH S=H O3$pz3+SEDVlAk!tS#t )dݍ]Xڅn@JdU($ j:Ԗލ6{؁\la r"xdız/[oՌ#XE U0''ȾD*k0}@I!1~o$* ^HAΠRc1#nA2VO¥k}Rg2[~ "J_N܆FdD[Yx0Iw7}c@60wg/28.d<~ےeE{!zt<0苛,,,bٖ,jxR< ^ B$ ڏO *iimr'.bO"7OoJ!^(ʚ:+Hɧ+ɅWݸd-pB6܂.wy, B K5cݝD ױz# z п?*$:WGcDgC܎^md] |/DJzܡC #yf0 0]/>A䍆eEtDI< ^7"f{#曁kR?"R eHU'("v:,ׂb?'z@t]Iskk*> !cPq*C?>qZoM/E"/V]jٝ$)"+A&n'%ΉEZ8kW3#$|h 7 |%@]L .UvC)2mJуX4 ӳ}Bl!d}  ӻ'[u`+ x [q;u?@ | !%8,Ah2M@I3q$oTmjՅnܼA+dfu2(߁gp?elU5m‘\e'rbuRWKIWەe W #naIlBk~fgFV8p)KI'"rA]^Dao y)RM lNs*5؇fdv#&.i=vrR7:{(G&HzTnB CHlBR utK!SpWܼ(Jq}rQMV^E#F– +a B V!EGv 5%@EN]Ed"Y7Ԡ~Ԃ( ,%Spl @#'SqYQ JEG]q*J.Ҍ; h,E)T`)")dDeqF@\( ,%SX*+7,>EIB`FeE)#+d' J+%7BXn}x ,%`#pA2WEQEI¸_IXQ\$EjEaT`)$pJD&pT`)$ĩ(JCEAY"*GEQGI`UiEQGhkEQ%Q((1*EQEQ< +fEQEQp+.%h (BnVR]Q G'RTWQ%Tlh IhCˑ(9*q?1TF *Lb"B`q(JK$A`}4.6BE+p+ $]QC.V ,5h6yE)Qcvy/nȡrGo| 0ƻˢ(J\TeD$ep8qZpoyp(>8DfnDD ,BN!'ݤ*J>v2r|Jʘ@>*xehLTn`}.J4PQ:8vG=(9xEfk<8^`}F]edZFFe$DVeQ2\ x̓cݏEݕAvb@Ʈ|J1q D\i<2`)VX.`}k2PBwlӎs $RQ F9B~w@%,t0o1ݻaZS&WѵEU(N<4O8r?S}sR\nmP-@c.1@DIxݸ77<<&ܷyhp7nZj a)&~YhSŏ/u X7{] gWtӅ|coe]^?&4FI"`OD v&=C"fąR\ w5g/E>{( ,l?+oǽp paOH>br۟ )%xAܯ;wevWC~+$.>i|>)%܌7i ^AґMzQTA39E+a` LlV ql`12gS-ltq!%W9yDdl>5Hˑ2(f+q\ )Ya,p+pǶ-4mp{"=ZܯN\QrdŬ+(yܯޜB75wĤ܀&ei |ob_6U2}| G(^Ӏ;X٤  )Mjz]|4u02J`~U~a#"h%؈' |- =@RNl;ME9 i>:% Y12K6ގvo`3^"cAǾ>xW}2lo#.n  OF ݏC|Xc4r Dۑj$96o3~7' t "d ,˾#+#@Im7ۄN$Hڗ::MϽʑ~]A?<u <%x7TF0;HZe 7JV@e݋؏]_?r}G#AEVLzUĀś`si8RĢ:x+}>0w)ETo>q0'o#]$5H cPV)Hz[PI n_0כo e&>q3/ 1~L. }GcEgįgb@SL1Q 6HEiM%&(U2&zI9֥7`y!4\c.ou ?k0>,9OW%;l ^,kazHT/r(JюX_8Mx_1/(3q5L͋XH\} ,/>EIxCVfH(WC&Tk}bJ1!A]5vEI h)Ań)J <q@)&2Hn|)9pBO->`%| ܮJ'شdfdL_I ˰ 7"KF~|&T (HlG+(ȫHŁ|&{]G&V2H*=Anfbm:L,QBBbL~9"i,D-׊KRͧу_,-Y}:Wes^hE{.[ 6Ċ^,eFBT^$5H6|IL,16 R}=8IXf!u><7r&gm&?dNDg1oVd"˹A,Z)\7B22Xk.ĐT*={D#WgO<`>cvz~پ.#'kȪlAD*$o*/@ !%V E9rMR%~Akݕ4U"٨/Dlq..$^;" H@B&-.e6)ݰg5)Ws& HGx]\i׭HɤHH.{K|%4%瑼].pM4-$ d?=H vdx*R4Hzmdrld bL8sq\!}_GoA+r"wQA)^Ld3) 0cK(d I+qq;$;V DL{;_5dg&C,S;!+`ŅoA&vp_'"xoD~moQj:pb,pY,y@.h dr o<7!cĺ t;3Hٛy= dlDzV6ErUmYn)Hm" p&9"qc@Ȼ؁#f?@ѫB8c!;gC]o H'ދvdCϰvo!} }Z C+}j( &"bd1cp'QFADƕO"`ϧ#a\q>걯O燑!d "_2󯲷rڵrKqHcSvթPX KK~OFf\q!4 #~"´ꂆta =ȍEj 4]oa }2r;v{F#y =D7VH/I%}8xs DDzK]\)ILȿ%tEXtdate:create2011-08-02T17:53:47-04:00%tEXtdate:modify2011-06-02T09:47:23-04:00ztEXtSoftwareAdobe ImageReadyqe<IENDB`2013.com.canonical.certification.checkbox-0.4/data/images/logo_Ubuntu_stacked_black.png0000664000175000017500000005557312320541306031057 0ustar zygazyga00000000000000PNG  IHDR@&~gAMA asRGB cHRMz&u0`:pQ<bKGD̿ pHYs B(xZtIDATx{UUoK4lJ꩞EhԓdR2EEaQQaH!ID!DD""""""""0 2 ԱrΜ^~t^kr}څA ;H'`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F_~[2\u{듟HwD ]Cs&4UAU]nW ȹs[W?-=;tIRoSn H'kT?tR@lgiw+8&]` 4Y*+.8!]d ]!g  H7!\NG H,yUI HsgJA>Vz>tف ƉsK(AZíW ! ID K⢊ym!A&=kQ $_Y\p G ӓ*UH ` wb cH ,//X/}D Hy|rPyAB r|`9 !@X[ꄪ]/}hK$~~TWjo$hL=m ŷA/ScvB H=Ap!$˧\=>`3I!=#tK r5>l-ɱL?>p+%:rABΖSliÇ\"@ Z'T]"$j)8KB$x> Hz̈́ټ. ;]z͈HJ i'HOp@`"_]'=fHVÉ\!@ *I7SnƂGDW->V+}P=$=fLn'.=fԵ$ @ Geyc| @ d`4ac/rk'YQA̻yw͚5ox|v[ Ɯ>"z'IOZ_riqC!F =NzSu׎%9i; 2]eiYJfdΏDžԼ2̆[?&}u(kF wL.H9ƓF3)= 4pFNKndeܣQ#}e-=xVH%]CUz&ѥY.Isde<]~Lem^ąGKgr7wϕ>,e-~tJxdokUå>(4YiXmN#xrZz&1ڸbX+"}IiɅl3.)?٪Y1 @D\ȵE{a?y-#@|\V9e a<.eUuٕ҃Cq1_X#= 937 5&LAz kt<..ҽ7=CC vq9,KwAIr vۡ҃B fJ&ZWo-ړ GYB V'qIoZO]:$=$!b5SzӾΗopA+=굯`N&=(b%Kࠄ@f >Js߭>遁 @|_)j԰-~fa^A 6'pi.~"顁 @y~*UV =8 b20ZzeNsҕ]oS4TE;蠌 @& ɣ`8ΕwVޑM/~L+OG+JL!̑1w1<Z+ɺq-, @4~㛓`t~HdX(o8o_" Fl{={yaAJBd |l{3|ޢܯA6 j\$pg5`B~@v \h2N}']oȧ[;g d 6JC`S[˱Ve#CusZRs+W:B)u;_rVs-:.=\-ɏ6=pa]gW߰@ plE:$*1{x[lTQְ&l pI锨$}/95bSw_[< d$V0a6o踏TdbyƓ*G߈兎:"8tRT[ N{p;kf {ԛEx+BBoF[S\#\Z,}JZn0i0Nkk{aZIW\g_e~8B-c?JC{{,dz{l o57ke;L#jwz+ęҶ~#3H'D`R<5GS+hA p弧N#)'|bu\AԾ pe²ފqxIBGY`I G͘e_j5_9,|{ Ȭ%fh0#7L|e.>izlJ) TxtşBuA: /&^n40=4l ~2gpB.Hv"mtz}杮db="] ;O8̈;lQ*ɒV{g#ҏ$' '}^U4pdQ_}+om#ܢ+Ο1mȴ ޱUmy%l|XnZ3_8n uwz=Ao:XA yH/CE&o:_}/jw#=9(1n,."Lj3%f^y벫m=v\4 @`ھQƲ׿IĪ`HF iʗFУ --r'C )@*ǭ?_bdYx1F LiXIj~]bH&N m?&Z~@dH>&^?t+tE҃ GT-6}U廎&,=Ȑ| h85*>臥/f[$w翻K3$xM"uA/w4g;w毙轳E'B з\fq ZL3r~)=Аt>齯j3d!=АtM=1 g0zy77۾{#;x'/;籱=:tw}tPC жwGI6$]?xj o|}l~k҃ F@#''̺]"/lM.@6sXǨǫ=ϵz_j҃ F@K z95I )lQ/K}ww | @93XQ'w'ȐF{6o'onH4f>0(4Rmk?*dN`F.;gTF},ZG@Ym&}+ۍ`sKqC г?>NGc&{yY$#@cF?EO5^xC w z_﵋6&Y|ⵋT x i5.fK8$-=^׫ zx<%WЇfyٚ+Z:y﵇sX|])gЯIh!#@eϹkA q9@@7߿)&dה<-=\t9s5X~ ;w*Fz!Xs2X~sdn.ٿ҃E@>g z#a7K:$7z@9:qjtĐt8qMhsZ?)z^Cr /Zo;ߺޱ*"@MNպ0uC}?{?NZޥ%E?vH,>r4%Uy k;wly?LwzTZ}$fzk 4<`: &?d,ZXYwzTk '=X4/[^5竢IS٩{$vzE@T˩hjnxνqNumxڢFYMDl~2jzSs-ڇrXUE@C4Tz2Xo~_÷{\CR n*CHM*s)dcon8uCR (hs/ך<Ψi_g5e遇"@PFu#¿t|uF=.=T{l >lwͧɘ=>nf/0^nM mXh}͐xH*љ3%ۨ?| oB3z}QS%[y)~ kMIE 1Om{pGכ6NcnuFz!Dh0>C]v~TiiLnխհIE :Lf7Vzvo6enZm@z!DTqcWݵ>O4_NJ<$/TwZM=2 ѣYumCR [{)]A3'h]K沇5kO6㸈DTo [⪼ qƋD洳ߺSöx-ƪCz!DLs̺Mö7Y$VAdiN;K[\9l{jͮAmC E 5-߰uU`mzH(9TYX;l{[**)ց1$RAd;[6W۵R,6|ѣ{o~f͚׵'Ad_;[68&'7fj k+GAdugчVZEn׷At ' E:Vn ͧx^`L׷At!f(zgS,zjAt[y[kķGj /z 7At'砫FlĤ)} /$[ZjSQLmw/*7:bJ;$ wYOF+7_+9J'E[ ]#=X4u`V:ŕc8uXwK;$ ]IŖ~;gR,Xxݏ'U^>StE~d Ϋ+z|8۩Cb ug_xuH,:|ڣۻ)]w 15F{<.[htH.:^)e&8ݽ;MX`ʮw%rH0Z{^ aY1؛ %=@z!hoI,H"ATi:{േv3?gNL/t赋zC'53=OK8$=NߚN}4fsIF@߫&}Mǫ LvnNf @i+8mNVZ_>VK7$Meu&4rў}ීWK7$Mo ^w/,u~{itr'Fs/`_{=+D#@SzyVWn췗AhC ygˁqo``C vn+z`N=ṟwH6$]=Obn~﹟:@~~*=ؐl鿆s?32~r jMk~k|V顆#@myװG~vRBz!})iOJޭX;{WMyA@ g2 nW>wWVB Ywq_?OX,7ޓu@C w\k~2ک2W@ K4$ncU>x{gy 0d19N_~0C 0{EwIȽX+,]#=̐xLxD${pG_ ; `HfΉ<7ζۭ;b tBI0C"m/{oo??3`! >؊Vv=Hl"' OJ/C2XϞ!C/8*= LIt Ȝ.LmoFN 0CxNN? =hPn[fRg·[)WZח ?+7ͽ}G3wzLޞXJH#6 \n^t&F@?kɨryFF؝BB;U8͋w_R~bCMO}_.*/ y;V UQכ-JU:m[Kwywt%FiZ GurQ/.]Egn݃>vfHNv~ (r orgdI[wiB6ٮ|:BZٲuct\twryb)[ KE绖k_GցKM5a$"@` 0*,AʾSwjU"[·t"@`i@ ZϪx/]/RO5LByS*c1Uv\'yZ4 RO]Ҩ7N6IJQ ~Sb6|9%:|t"#:>{-_6WZg =oQO^]~`{ 'B={܇=U"=6 R.ױ#e;ϤGSz !m8)=]p{~T9CK#N)f/ַ>2l^WKz!uwQ* m>-fI"1QV?SOLDzlN/}!@c$.+;{cQ~۷oͪ/ȓYFsﶽҵg{jϲⴕ|?VCC)Dw\eV;ewLmYS}[B p|B~U3P ]U+ GI}#Lxٲ{݌lzGkZA|ɡ"@o/Oprztm)#?@`+k$f n+{=Fjgg 1At#@nyq^_/JH]W"@6Qu[))m;x]_#c@9KCh}7=P9;brIZ lYwA!2|^ڛ:\CB:,.W+Ckx_e1\H:&*L3ڊt=0N2_ olV?xiC }x4vSu !@-QQ^w*0oxf'.zGa&z 2[j7``3/,VE`c_I'EEϞ6ܮ&{&͔j}kj : pu837Ż_uƞ"ν#jǸ?o.N|#BCI"yl? 7|cC%zQKfǔ!I{> d $ 7Ϲ׽mO֭"cYB !ذWYoo\|;_#36![BW4)/]е/`fEgCY@ K炎`z-woӢ #.֎϶s~[4=eg2A\Hb]k[۵o츣;N46ꯂ*ؖ)҃@ .M4xX)l @鹗75xuZzh #'UƠA r@V Qu)pA^^w%lqޫa @m ##A @WpQ?[ׯy#=$!u$.q]6 ="!$~z̷._՞1;P%=c]n;lq[/= miAcY}n3Gz( co<.fe:M[TD.cWV.\љYCªn?JIz s)OZmˮ^F @;3/-ve. ~5a?"\/= 1_p X1 @I\FmiŦDurQz.qaVB!郏l"@͡^j,:@U#C>o>bP:GV h}>~.aGf i]>]-NkY郎"@Liw>駥92_I2hy]Ë8g/I2&\w%}i|{Fz.Q+b}q<郍l#@ iDçeJjd^"N1˷7о B@S*r~w 3{gr)śX8}q,~ nD,\rZ@~ vd<'= ZZ To>B^ƅՎqWgg7\҇@@ʛ\ċ@@̻$HL> r>'AbE^ 0$xS"7Hq-} n&}H#d'=fJaI^zM"A"q"\)}CaSz N^<"@Ty郇|"@x*Lw҇9E 9ۥTB IJO)ӃB QD`!C H]Sr HƊ!A|S#=/D҇ 9G yβ{J($z ˥@ 6ZVC H$DGk=+)!}p$N~qF HFI Ta.#@d˹kAB Oe{R\ʻHwCzN)0 ^zNWPA t)]R 2)׿fL~rٸ>SBzdӿ}Et Ȉm`m 箐B2ct^P/%H'Yqz E [-i .`A^$ c.RVdяHC_HW D v,L[8@ ~Q:*ʨta7dڙӓtooqza Ⱥ/oOIpA}jٟ; ]5yqpue]sL"@'_hz8=; ąAzQ;yW\@ ȥso/OK";5.c$x+IVQox⣝,\!@!v|ݳn%ږn_;H @+mm5\_ڮ>㶅Ͻì#@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@Hf`t7 w..!@h @{<ҵ`lua$_^V[ɗ$_ Bp3H !@ /g|!@8C $_ Bp3H !@ /g|!@8C $_ Bp3H !@ /g|!@8C $_D{ߑoZV{qH)$/ԶR=$ B?, *H ڥGDpH (qRbH P!@Ae!A@%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@A |!@P9$_ @`%#@*@o|OmP__zմo_'w*Ne㇯uut҅C]_l>w b ~۸׊M>Q]Kc?6VT7Ocņq9$_d`]Bu3_8({ip9:R+UO19$_4+U:\/w1̜պqyg{EbE'joG'@ο1`ulnfYmO7fYn3d}*kQ4|_Iʆ\2(Lw|7'҉rW:ȹg7iR+n |V#3ҽjި瘳]Oq}]6sϊrd6@v5`aG96GVD峢) # y@$}3~E&@`YQTȦqfzd~ |V#3rhK]'@6sϊrd/@9cRgM峢!gNj |V#2ծ a6sϊrd 5u$TOM峢gHŮ w9gE92 wRw9gE92 +t?6MDn 0G(G&r ѼfQ>+ʑI 3pr$($@k3[(h |V#)qu$Z+ |V#)uɮ M0m($@Fx` 6sϊrd #Htv&@`YQLd` /3 0G(Gf$m7q'k3Z)@՗T?Q>+ʑ Zo~ݥlunmlHP4}CϾq+7ҽcݪsG&In 0G(GfF8M7N|{FtFhTθLm(᫡$ANCYBof/PgVȼVTId峢qF#oIoMHQ֎Zމ[Vȝ[gE92S Ź;tzxȿBudȶk"wv |V#3R>qہ |]J峢)UFK箍x^,  "wF |V#3Rg#pW WXRS& |V#3R\n ^t%V&$J?GحE(LgLk53\D  !A^ sϊrd1@z0ʥbKp?=(La4ᠯ'CgBț#@J^C#@`YQHy'=>>Hp}huhl9gE92 xmWXucHL5F峢 wV5j9gE=Ej(y2!t|2İ}[Q>+ʑ;l+4E2Ҟ;4B峢7M}mKH3Bj1$|V#32fz  eV=7E峢 ÄH9B"sϊrd<@Tq呶CqHmGsϊrd<@WqV @Y+j&Q>+ʑ};ȁ(!@ʚ.m1|V#32bq e mE |V#3FK ez3|V#32B6B7M]ڻn9gE92 aKbty($@'” @M]!@`YQLdG$W(o@.($@>|HmFtsϊrd '£ @*T6Z,P>+ʑI VOr @*X.mW 0G(G&rAm[ @*($@. nW]7C峢og[ @*Qq+Q>+ʑI\p:ׇnD}VW 0G(G&rQa u2*|V#(d *:RBe;Qt'@`YQLg j RI9gE92 ~Rrw6@T򅺲Gm9gE92 B?+k 0G(G&rIU +>V|V#ڂ]HEwFsϊrd ܫl2JHEʆގ(UC j}!'@*W6Z 0G(G&rɦ 5+Zm#Q>+ʑI\re )z6F|V#~jՐ MV6T 0G(G&2$䵶KCNT4S6B峢ȐVeSRz5Ņ6B峢Йhm9gE92 !-!_'@*Zlh!@`YQLdz }sRѿ   |V#WJ u*"@.#@C(G&2$ !_'@*zPrgE92 ! V'@*zDrgE92 !+ -RbeCeH|(YC wmBNTeCeH|($@|lP :R gE92 !km HP>+ʑI J&b峢Ȑϔm HP>+ʑI y_&  |V#r/b峢Ȑm,zw  |V#3Okg %<@QY z^]? 2dP>+ʑvuYC 4Wh z^殟Hb򱻆P>+ʑI yHfatzD4 e"@C(GfૡM6 m!_W)C,@rgE92 !mCׯrO y]CH|(l'2J]!_Wc)C,@5DćYQOJxԨ`?T~; d5DćY C󸻆$<@E*uZapi @ʈ1@U6]CH|(rh]Cv j|}CcR6|P>+!W@l)m o;W)#Apإ1 H|(*]Cv + jgB:Jcܢl8< H|(5l'ҡl2|vQd "@C*Rl=rQqgTl'>~;(RF:w  |VZ\9a$:@ @o:iNDXz[ e @CLTMOI+It|XP s$9iNdr#TȋN8k2S94 WY=MnZ}w{4@pR_fg  |VnWM)*It/E9SѨ܀xs )7rޖU=2-KćYY7n-e3IC Z71Nwk4@Vn:w-u\=2ܭeBćYQ/!e3Irܽ88r#3Ԁzd!$>ʧ+5l&^1/҉[8쪫9 Oꮷ#ovw g'!e3 @L9 lw<%gzn!e3 5j#lcz\5r#z;X=$>ώz9^wC)Ip_T(L ri]( oJoW |vjGJrXˤ"z:Q/pvgG}P}W([InW(SQ8lNO]]e.zp8iIćTpv+HKWsv=T#RWgW P>; 4jGJbBhstF\-הQ/]wK Q;H|(!3;I$6@BSi37䨷y ݴ];B.q gl Q;F !|8z)a^dr+!`Lr g)䏨9Q6Q/v}wt7rr+3uw+~7 |fsӌȏ0#nI}0M (nʑ> i峴,߀ym$3@BJ  Ku'P9gBy P>K[C 8ZNF2Dԋ]mH)򎺸[wgBćY _6 ԫhMCfͱuq-)=Bćj 7VM$2@&D<].z9mtq$>֭!jJl"65΍4lny A->R & |$)e VQ,kln~ȶ} ux\$>VoTYb9:e SP:u[t:ҮUqYP$>ذ.sS6VezR:8dcֹ+†d |vԆ@{XI ۜ s kCj::N7BćY;o:FOXV^mwޏ0e-jvrc/ھ!v|~;]KA] |.ԅ(:aӂW =a(~~$tABߚD8/mZ @C\x*ds%]Ȼ!}hݡ/X^+y(UQ_5܉9Wky0H|( ~_3yND OW6^o^d[xa08d៚2s"G1>l2G ۈ>Eg {EEy񆊛?G gUos\Գ7S5ܨxmdi߱~y[phyG):4$R=w=Cć@_1پr<(T6.r#9j<z\h[;՘|_͌H|(/჻u6}p&Hz.Cq_WZk^_[.hzvD=?8|ұWVg3/y݉ÑGϑ5 \4a[ uTEk]2Z|}(O Rko7 u?2rLS^ÑGϥ2R uqVK T.5ۅN;yD.kiF7?.8e|r ,eRD+3Թ+!2"# K$+3ֹKB8&qBdpF,nSrw89vB{q{I-EN7H.]+3ڙ $<@ ^Eb qVcg᭹;yD;ݫ.%;@uōoG ~Å'z+|Wf@+Ͻs<HrT=HNdpS .÷2w#(_ j~$/2W<|`?Kl~gz)=72ug<̀V(_<@B$lE?'@w:'{m? <|q/DHq$zLw7Vd ohvt hŦA_WB Rڡ<.9vvy[#Cs=DH%[wwMG,ϊz"84yq_.e @O\v5w-{/+#w#Xg-<ߩ]3b."5B[y-' 28;pXlۀcSH̎ɏkޭ>&lO|m,4Z%+28q]@MqBqؿk>^kEvcc[MeK"rr{M;8fu '4^pa"˿.nvo[qCknRܼnkZj%vy'\4q1{z8W8AnvW|\Dxrkl. VwGۥHq~nRڶ*_T5co|fdĶZuF0[_s͹aҝo.c6 9MU-흯yC})W͎g sѠ[1=fΞ3=6ʩ7{&.6Y8v?'Dx6s'4*5uMf{?z'ԆA[mnzC3֩~7>mG?[1 'Ԇo8 wNiQ ͠almj|DAۻu]K7?ݸ`bNӞ=_}摻;O>!'ͼk~J?}k5~bGa֍_ykа}k'[c;Z/e.W|Ɍ!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@F`!@Fѹss%tEXtdate:create2014-01-09T12:41:12+01:00mx%tEXtdate:modify2014-01-09T12:41:12+01:00IENDB`2013.com.canonical.certification.checkbox-0.4/data/images/JPEG_Color_Image_Ubuntu.jpg0000664000175000017500000006225212320541306030236 0ustar zygazyga00000000000000JFIFddCCX a     !91qx"AQav#2478rsw36BYt$RX(Cb%&')Gu  ;!1"A#2Qa3Bbq4DRs ?P @(KKޣ1pd'̡,\l5hBb5(KH&'( @z,k7~XEHscxRP0 K*^Cr%?;`>GmfPN l穼y:,/(TTCKN͢F'aE cl_|=P @(P @(P @(P @(P @(P @( Ŷ.[6з"@7)EUuܨpEbj(bq{Ųo][i\c>^/f5z*;ںCբ{dH;5N{ձWR6䍭9|b}krLwN+f&tՕQU*݂LJzS䖇@[ {r{\ZVusaliE=woz5J4TU\9cXڢL[ Y+XZˮu3PMu8Ŷ- J6`yEC?""5o[ Gex6wes۱Grk?2nmm qiͼ[D꺒:a?jy*׵߲&>υ]PVG…|#y>rr{mOu"ȉ]Gs?>Km#H}w.Ǐ\x\x0}U antXLmGav0v6=#iյa|Ъ'gGZ5ΨQGV%EE:Hj]ͩlrLr'*.  $FښZCr:`g bu+Qht_k=I d* }mk\7lV~jEUENNV/ܻwv=RG?9~).j˅cWgf$/3&Z?Fd- IUp:jdL,j,]m׺k+覅9Ds\"9 ԿޕW5{%"=5Lk?6\sVHGbHÊ@(P @(M)k;-@b@H >~~yZpk{?jڀP @(P @(P @(P @(DG`ƀq)`*m @j-P @(P @(r6@qUtdkihYggd0dB8tu4D*.题(2wm׋Ki-,;X/kSEUC9W߭:gOPzzxYHL|{R6EvDs! BĶ,$i1Q.+U/G-c,hU@"d1լ],}z*,otIDUGԽ?)/[b+Fߊ\|\9r#ɈDES7'*5>j@&Co%#ܔ㚮PB,&uQK<<\##jL5gJMMeJQ+V0QqGNZc"83x7bq} r#;v]tfHo.'.^Ƨ1祌"wsfɸv?r>Y=|='*z}P/qm6;}%k9F^MR|f-QQ\s36u몛3{sVu^2kg'V'%WO\5<\5mbe+I+I+"24ccDz,;q&-;2K;e.^NL[TQ mo&BVS(J;{BZLu)(*Rji%|$/~dDV"H=E+qJܼj W+n4A;*! F50Y6^JS~@~@IdU 2<:J"c%nU%MwC۔ i+jRvunz{_XR2 10wtSqQFRUH,e*c6r/{s{oZr{%rbF k~hDErcWzUK>kQOdDWG#ښg/b#FƼ~.;k39,a1q |ȴ dpՙ RtMrs4] jʊK#j(TOWGդr>>UsW((֬} I\dӢAMqk97Z^3w{LNXnbKyNW (]9{{j$Heh\zwL9ra|aȿJxa cdEr"FSC\ [.tiA"L훅Lb2A5v&:N+K_N;f)L)ӑʩdֻץ{KZnV$>TEVDTnQp;9ݗ(*O:P @(PƛSיwZ}>bנ6/q" ;b(0@h?(P @(P @(/e㘿1}=?؏B|9袨oK疀+7t?5yo hP @(P @7˵:uwUoV}pwھ$c }ǎy윛C?8MJn^M8", K/dF2 X| rx08B4g›tUjZ~!gO~=s'[\&G.uOfZU%61}/Mq/z)k_ ۡMjȞViF_RCg1Q;8h0' {rnj7ZVX¶5EeV{m^j_ϞI jWU\^ I=IS#~$Ȟ>~^~*\? )l}A/ ; kü8npT:l"^qW۩to~.?$-}YݘV/|X>:}LLJr" ";?-A2|Sx_L#*|A7FxzfeVY){;tdF;![Q!kԘH ſo70hwLNXL+Z}uV$VxGJsW邷_S&!sWZ1- OٟDI엄 jjuttRZNEUz\Nf;ZW? W]rj:|VeO2rq(`v31NJeR;_|3[']oSXn݀kK4ޣ88WrN(,g CV%NV'4oy=/M \sEళr%p|<,s .<0@Pl 5r'#Htr$}*b! qn>ߠ4.#]muX嶠'̪hiRflU1t$0}}S@(srR4}M! 9Cŕr9fU2f1P 7}Y?.h m\֥r4=o͝Tэ_UR (``jƛSיwZG ;3[꜇moVvn &pPP96})/1-"hF6fʮ"i&s@h uNUfF\P dS9rz)ϼ4:]"&q(ν/^,AqӴ3ZXfpG ^cs ACqx/%$2 #1d@80< n뉶>Uh9eZ1rnXE{޷#na#P; heOMDZ:K図q 郌:C!pbd)e\++ '+h0]MZưtFf6]s&8I ˘F@Wc׸Qn<"@ wq\, fDyU!W&!pjr퇌U2y(LPPNP),Vo@O˚3zWW pM!Ƭs43݆d@и.JՔԏ*"Q]>Ps<H<)rn@{P @GMqHоiN=g⏨@BǐWL吭hgf'pr8Z~&)=8!_Vo`}31!\*zTWz`űrP8 _9!J ՖЏ%k-6f: ) 0";Ƕeu'd[ FZ7]-pkEHͧ  h;_ &鑸qhpҾs͎D%L1o<1QEF \G[a@7$2M[\7núa^0ME[:]<(`A)AQ,#C0{V~Ѡ:/y8]%pH͊!vfNe8cys$bXuc@IGrMpͰ)BX1$BDq?1P{L;R?zWn5șTIAcraM?:fpHLK 9^`$Kt8XlϸsOy"x{^k6)fߕGOJ*C Km ^w."3-)BΘ 9.}ƹ)剽܅nptml`,o!@y7/x{x_E@Dg"ormߔm^7Dֵc+#y4pY>4pY>4#qW{?a&r;j.wzZڸ0y'L5dH m {I!-[CVYkd83!^osD;ITL!=A<Օe20t>C:'}Y/f'ouiUvkL}!q+ݚakIhGrɺcࡑLr}th;{OKu):$ox0^x6L%$X 8UsrhN!/73怺W.)+1<}loW6[)E &2lybaL~%n wD݅ߴ`޸Uw1DNab=Lglx Ko(1Jx)iUD`Ûa)۴*{Z;ŴubE,~);g?\цjxNV%sJbm=J.D2\"Or{f:yI+*%g1LWSkd\cc1S$=o` PP* Y7f؉ű_힃Ҳ|Ε$,E%W#U^ s**ۭ5I[_䭺U**tjn5R9?#j3+b|D(v۹$gO9~3WQӵEL~g *m"om;U+Iw)&rʪ*~ꪽz*h gN6G{'1ƷOnbt{eR<~ 8Fo(Z%B@.{ULBR'\w ɨuޔW@~]^ۧ͞?ET5jnDDޢ5DETiD G t~Vy8Zyoao,XMb?JJ܉>7\߯d(@os>mM)k;-@b@v;=>:D(Q`{Ȟ\sI[7 ҕ#A$VE"RqvR9@(0 7 a|@<bO BJ#X\P>Qh ˨?:P ]6a.ul-<~vP@wv7qp7!@7 ;!"(EPT誙ʢj&aLQ3P(8 %Ч }fpK9;sc 枪*LΝuؖpc9v-MWu@H@@@{@CPhSo( ^8 󙾾* ~}~Ca?Hӳuog=w_@a?Mpx?%HG'OыZl!yh :?\?<=' o1Tw[0zL怋B-GG߹!m1_|/dRriԎ:2#1U騩g]8fubPyJw3 a? $U}A 0̥,f;\ؑZw=ۂ{ug)m#ZꈒE'H DJ7;F{ +{=~ޚ*7Y OZ)|p f.F-pIȋ25?+y̒7pmڹV:7\4T{\2ٚߕw;QEk+ӥ}l{$oK} v]Q}+Ji#G5G$Kq~lX-tP \a 5 fm!r_w u$%xA$e:"5GJ]:[_u ]mZj)_ &r9ήL?dj?ۈ14^Mήn265 TAM+-56ƽ:Db͎Tl2,yppLi.D8ǂl؎{u.?3}n;x47?V ⓩu~?/f|9.]+Ę,-elsZ&`>ǘwt+tգt.u;MBA[+WM-CXX4$=26#y| Ii[^nJ)+*j9OQrjRZEGSRR#{BGZvZv~Hs(Κ /3 VgitC|tFt FQɣuڲMxM%;#JEU0+T'ݻՍίEW3ntnd2VseơhQkӣ:桋O +tؑ+amDߗF.OU1d XqF=r SpT0dAR i!1.hI]5_W&5TʩDHU2S5vm%gI%DwV9֢>s$bѵ)܉~H_ׯivqfnӠ7r Y8=>@(!~}{|n^P6}˱+;ƛSיwZX^#X@9LǶջk wˤ 8x"eH1R ,c`2Wl'^ EM; Wv:TJ"lRtQItb 3Ibn>#EUc-`HE9;? WE@Q%o,rSq^psǠ;VdrX2U(ѹ96Hd̘۳a 'zㅗg4r3~Y|%Pl֍UxE!rMMF)m|nz?ٿ%ړ2Gwp]ܶ+`#ni2f!z!WN|N.ziY)f7ԝEdfdEZnʵmH*iʩx*.~ n"冉`$LưpHF` p "DJt)"h t;5M 2'sU7kع?Р=+҅H7Af~Z:.!3OΔtMҗRBDGdN3,-c Rض2j{̄jORE刜D䋣)sދq`"He $Zزp@ȝ%N*IUWn1);׶"2߄b3cѢ1sn&쳥=1pOG,=Itmq]xzʑMY5l \$r*C$C9 =tNpݍn@pĵc#OV=α\:Ա;*/i#=?{4/լq.vz#DEEE+ze^ofzJ\;a|dCGNc$ ^1NK lrݠ%0hWrI ,/t2ƹk"G5QQS?QਅHFr*aQr+U;.Qr5 1Ď}9s!s~Grsɶo]ힰt> ۇ+8c9<音->u`h1v,I !KK 9VCX_Y>]\hG# fsY7iAd**m ͫTYCw c321#{gjr{Q"9Xr90T>6j]nZ]t4R[:VMLVhJq9#c^'Slk RnҶߗ -)0006|Hf U˥SL(tSDq;wAkӚgUU$n$K lD{G=Ew;*Hno^:* ֋L*y'idV&BȢcjHDք4uI$$m7nH!^")kZ,'x8Hy@ J=3|"j7f=9F포*^lHI1=9rWHm*mecZQFˋ(s׬;vm`XJцco0Ol2=E%*cTYUN20ڭt6[uJ;}6 mDDdlN(*s\쪩Ew7WIs^gy/Y9~n#XF0it+dL9Ř}Nރ }1gNm؇RPO{!@p{Q.G#h@F/nN#^gy9j(ei=k@m_pP6Śy7OٮkwhKY 3DyA4|7dl=` 3| UR kH~5z8^FO [r2yveWh8{46M9[BB萂b:Sr h{ >h p-u>زR[ rUЫ ʢP\x.qi͈3+Ҕ0ַ66:~}V><bO BJ#X\P>Qh ˨?:P =6J^x Ke 4Z .OzP >x@}y( zR-'|=cå Ikj>5B:2^AeF @vBR/PM"\jK>^muvhS).l=(ƐYvdeLʈV< &\ @-!~s7EP,?-Wn.~k@(<>A( dz@=7a%@lr=@LUx?%^"vg4ZPh :>|?! =/h qmI!Зp4;)~`k QQ}* ˵|ݨcHfb=OeÚ9f;Wi엛sH襍9ݮOr9j^5U ꏁ|r7V+8QWE9\12J׾ʂr:ӂIJUS" S?\L]z[_nMt.˽k7+`Dr9 G5 3# HcFoȈ[c"oVUJxrNuU8FF⤳;Uo;#& )O=i*vGVuc.<K~8~kMlqgլe#VÔ[/ۇ:LxO_>vro]Gu:IG2ɜqI?8?ةs,]t8!.\aq5]\7JQnLHZm{tE.;YJUΦS0&kB6Ӥc1#tU~XOu=ѱHEI#E2pRuEXѹLL֖:V/jT)TyDn{"dPQ@A~KB\~5cEG{ 'ywF>fsz,ݯ_jm=dzK*9Ɖ?InG=vUU rweKX-mh[&¶Jd|-lCsYCUu c Zke;}QC 5>3sU\U+^ꋽuP'.OY'#Up򵉆1cZDNYY`w>W+0uNbʼn=@(_#0=ȟuB8]!YގF^4ҜFr(4p7畧[|CG=h}5=?Gz.b&,e9L2n Qf6QrٹhbM( aDGrpL%3̩?.IX"ojc8Vc$@@C)N kyoah hc6.O4(z@( t{5qYc΋~~|)gԶP3E.YТ |1@i@((~J}ԈPz36|SjqbWrH^Qz7Yg}zؼ:K/k zT^//Vc(ߜEzb? <=kӧ@oAqbz'@}6P WC)( r's~8m(cVw?#7t3  (}iů@m_yj@(Fޛͬޞt%z{# r12sP3E/Y ! g3@[#<3EW&gb'R" '(>PPJwk[Λk @d@N?F羫CoTrͧ@{ @W ܑo. (t]{Cg쟝( ~L? /<-r΅xV={L@(Gޛ6dLEm@fH8鵐LUxC}TR98l!q4}ڭOZQ7/x{x_E@Dr_<\ y㿢@KEo11kP?-GGˇG]1TP  *nxyfIh_yC6h54&>P @(P䏺zdUMRm̑ʡw Ù3˸o 4&kOx2a"p-r )V뵁s V7( Pvހ]؇RPO{!@p{Q.G#h@F/nN#^gy9j@8QӇ^ؾ!#PP 7M=hFP)92܂kv30&Ooc J<Poxݳ)&4]:XL7n1lcnBz(D;IMZP8y a.q""> {( h ݜSa9@5{sc`BL!q6#'_:P_ڀ,1@OE׿?D>]F~ҀPRo[(P,QwjS׾4̠P}Z,󋾽W_B7wUJ66?؏B|9袨oK疀+7t?5wO hP  {x~_d{@vYLA€[\# LDB1%Dwr@;( o1Tw[0zL怋B-GG߹!m1P @(CWUE6Z6nFxcTbLeS( QIo!C21 @m&ȱq[c 5 mT0rT @~j~1%D^oײ 7ro r6b5w1A$y<8zص ?5@(x :fʚ:t|gl4k&nkv3'V% 0QCK@D yPEa9\z]$T%0C$~W274q :,XHؠnT-'}İbHR,LdޒB DCD&ӆ8{0~N.Gg@wÐgc 7Ynnڀ ..!&\"TPn@[~G`vhҀP (^cypm@doG'ߟ.?d@k@( `m)y7-( h t(µ?g) PfP @( Ctܾ-y^@g@];ͪ@i@zG|@bLJ_ o@Dr_<\]yV"ȗMC1PAzҬ^"!iAdi1F @(YCl>;oi98kّSץx2ەnZ=3Zfs_COKcP @(H0zӷ |Efsu-W J?':蛘y (`( FZׇ|;90Cd _l✹!޽ÙJEd̢UAֽp!:t@-@(!~}{|n^P6}˱+;ƛSיwZ}>bנ6/<M5/Ky7TL)Dm]w#_rmT)dHr4j@Fl(P1H6u5#RyoVX4{:1Aǃ6 Rgbc y9#X}iowtlHD\*p19;>ԶHlpE'5I,g`ppBYI.zv R (=Z5n-o? )n&j;b60"ٸl 4SQRrPWc+\'%pJ9Œ.MJo\߷rmfиnuTs YґOR.\>ufkFyMr7AR~nwۿDNaAWOtVL4PB$k˃~j x?-?^u'J_@SiKl@f@\E39O^b2P @Rh.\Z8 }߾YmV(HT(P3yD1)c{ƋZ힢."F R( 莼[ ` %1L2y@w Z>{ͮO. >_[ \A^g [![:&72hʿCmcH; HP{7@e+qLi =;Zʋ#ʒf&fܿ^\rD@ vC t)pA7TM^jCp䈖Ff 1b$PKA$܊w(mx?%^"vg4ZPh :>|?! =/h P @(g?SavlxИ= &S3/'1%FK*0s&gN!^m LiI& bMYK.KĺRl7 wNo@OP7=90$@͠qb];\tsUfɁ83pJKFLELg%ILUk5)yx=fVw &*DrH@B Rg odi~11tŝ)RPF.S=vP@a|dLMrݔ`b7)@C7?MixrrqlQ)Wͣ3lDtM 1JPY. = ͶS7AC,1ET!}_֛\k[k".bH**N=hVxj>dkZVI֫ظ-23ݗɷ,뤓]ТT=4$R@XSJw~.|47|Q7eM'efN={7 5ek[}FǙP5A)$BII"R&e&R@ P (ր0nl Gj  3PPe5BSP؉vkU.EWɖRh+ʂm7s4~ p}p%q>Tʺ4w 'n6&q]2F2BEr6~w,3Jfe[%$V @io*\>5mVMĶ<f+5nFYGixIPJDz˜ހ!v9x@{|Toh/;Ҵ( Hh$8z9 u7`P}/E ]Pa{'V:v1,{q l;Q&!#d](gDE'ߐ+~Z:.!3OΔtMҗRBgB+Sfre7MRg]긴p}ڭ?ZQݠ3\JJ֜sm,ŗ,IF.]2?\5]wBfu~rnDLT=|iIgPwD&Yˌ咸l 2ŵcf ͼI"c{&=\o5jfG쒐/- +3o%kQ%mwIG)tpɬtA'QUM4BLcC##U\]8qhBS=j I"l݌2.KvJZ4V qt]M+gOMZj!M<[.Ra BE\Wt,imI<HE f"E:9.`W7Eo+~O2ŵ@by3\CTD*qw (C(n!|PeƠ23n4.9W]Q}q)LD%Rvgl,n )X j%!gKqwe .لn{":,l^13pq dzS!µ, Խץ.8k Rk)tʠ7 8xժgY˖HUU]$RM4'PQC D@q Z:@pl}9"Y;7c;erhetUQMBCr~.NuՓ}S9Ne$Jr hy 5I_lAE>Zfs_COKcP @( Նq6D~ՎTf{ו p3QT 2xRrG-tDE3 p`\&T| nElƪxXV]K rBCSH$e"κ qh a~fȡ ET .{ Κ7'H Tβl] PQ0h痹n+b8]4֖$hrE^809nP #@D;;?9J?!@x8_>!YގF^4ݼ%xmC*!01˵LgnAH67|Ss0L R!HjnP7NWF_%vP@Ȭ >ܻm֛ & }7mwovDDqdzq&kxE8Շ܇ #t. N7'cf !n Ɉ*Vlr+,FHYh N6tj}K%ʝň.i[Jc;wI;H:p$M$Ȓ,٢كV?r1*]&La+;?,^~ʪ2簲) _* WB$q`c=C3F(IyDΤq7ɶK"*Ub(XۦԙLtYLUc$7]1r,ELw 7n>sܠ9<GA bͪ`Vy͛Q*H Dvn;mݾzuv pYcQS("c"aDhv~b?@OE׿?D>]F~΀PRo[(P,QwjS׾4̠P}:,󋾽W_B7UJ66P}Dy'M / _2mL #0Z^ }u($᥅09pY4Y cMp>Y>]^p&QƗfg[,0dSrߪrQRtUQT>C@׌\ 7P7ve[9Ed &N1NJ `@2W4&/U"g%p>hu2g;wONC#"]Gp8ݻ!"}Р-w"Ϊpgf4V8Dm1:Y$өphnzͬGL_6n4x)h̊휶XEۨC%9%0 H- wLSC }6Gr.4=4&s|nE !IL֐Q@C@C 4K zP%n0k.NeaŨ\~Cp9JdTFe>R~Poj#q ~ m}$é nwINdA36L( vD[a4c 6sr}$s,Pd/R(l& ?h'-9>]x\ϿGiZq0T{U ؑ2ndoRȨ_.1)zrq-hmBY-oYMjڐ9[B6ETze 2#P ǣ#q,5ZbBFݮּ/؃%!'86*bR  㴬ͲnBZoA&TSD ۅvWn } w>^FCNX9w.kD*GlI)\oFLa5O1&Oo Kݟ5J=+`LeH%;@$u'8Xdo€<۝r@.8̱gc*Һmױ#./Wrt52( @Dß#Kh"{=qT7*۱kFXʶc9Y/+P(3 L*!fS/@RS} ziWzdOQjNc{Med@*DL}M &A% YLTPtƋ4ᘷx6EC,'VSc|{vPxfv}gFVq `.<|,;Sl;:z 2znx~hw; Y@ATEL {mc L@-w<(a m|#8BԲ ^MKNatQq7<5G2QC P @(2013.com.canonical.certification.checkbox-0.4/data/resolution_test.qml0000664000175000017500000000202412320541306025704 0ustar zygazyga00000000000000/* * This file is part of Checkbox. * * Copyright 2013 Canonical Ltd. * Written by: * Sylvain Pineau * * Checkbox is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3, * as published by the Free Software Foundation. * * Checkbox is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Checkbox. If not, see . */ import QtQuick 2.0 import QtQuick.Window 2.0 Rectangle { border.color: "lime" border.width: 15 color: "transparent" Text { anchors.centerIn: parent text: Screen.width + " x " + Screen.height font.bold: true font.pointSize: 80 color: "lime" smooth: true } } 2013.com.canonical.certification.checkbox-0.4/data/video/0000775000175000017500000000000012320541307023040 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/video/Ogg_Theora_Video.ogv0000664000175000017500000162554612320541306026743 0ustar zygazyga00000000000000OggSBVX#*theora-$@u0OggSBVX/ :?theora+Xiph.Org libtheora 1.1 20090822 (Thusnelda)theora(kIJs1R!1b!@d.UIvpk@HGәd0K%R,F"d1b0F!`, h0֕UTёPO ̌KKJ LJFFFE‚AAAAAA@!31pSa5u!bSFtт3tvwT'Fv1!6661!Q&666166662&66666666666666666666666666666666666661AAAA 3Bꉽf"G?r"^}jfWbB`K0ՆU)^}4̮؂ȣU62^?9w1+< sTWcAiAuܐek1x^ yh"cuk3)5 @ x]w(=ιM+F_Ţ9"ͯb%r({IzJy'Jl@CݺcylZu+1eG#FidR,>id<t +-1R܄֏^vj"|v}-VK&b'w@k^KYj#8UIJβbQ#9C6W8bǒ/j>FaП2[ E(,y?mB"\t-2{|H纊V/>mD<#H2nm0@u ]P9;DzJi## W`CӸs548r&yF \{Icp3%ΪS{,50r=2\Ta}{ n5NaVDK7N4 n5&6dEc}BF\g 7b.خ <)#wVQ!f (mK \N1f2Biv}jD8˨*R&spZ~z.+D)p=d:0 ЫZ4]%M܃&C:ŔȹӰY=-|[יŕ4tKсMFpZ-_ [Map#r,Y>e*C , S6M#ϓh8}u*YS6Ocx;D?mp71qZq1͵L_Yb pErr2jrEYb{FV fm,& )EԜ}7 _s yZhmR&H7y߀(nxH&gݱwT)|1$jD-:?0+OݠoRp[D%fڥi6LpÔ#YXޥ(x/`ŐT3`]AoռX*`6fr{D(zG+#wʔ~ϤVVF ə4́Qӥg!t 4Ͷ[슠)wbB& GQ8?,"$%m6jR\e߁&I*/?uܶd<*#$%4{8;ezq=+~`SPwfi3meaZ}bd6ʣMDVXOۋx?OĪ'YXg wqu6ژP4' U"'ְ JB*!9U /\eL屃xSA@jg@n"^ui-36s tqaI~BAd@883ȶu&|kQߥU#x!p: pglLGrK+y*FCt"ܙG~VUT8Ox&i#!eb_z.쎏R_, FS4Yot*V8$/J4Ͷ̱ѭ,k#p!:?JL晶"SQI@z;qϡ]7i3 ͵^ '+R2Z583^]QdQX^Lsbm iJ@F,qdD'6,Zf]k 䐀OggSBVXp0L~'vWZ0l_}}׾{_qxk}߼}lS>n󯼿}x7ϼ߼}uؾ7TбZC`4F9*}2!&naBnY7yNVXjXL2MGRL%a{z6JIofu.l{ͼu-c({e$ɰ9-7Оƺ"E`;i.Ӥ w}Y" 3$NX^Ͼ}B}}S }_~n}{a}}>׷ޭG}շVT޴u]>at}1~ZǗoTaD>}x}x}x}x>}x>}>}x}x}x}>x}x>x>8/r PU45^ۦKq]hv{kFFv4TtZ:t(x"FvGuDz+t ؈QLIWKuW>Jy;Wmy@:y05wfWj<jʼ$k~^5kFU#]XFZ;z8ϝT@/^{>@hm)@ +N*f 8tP1E E tQ8u;Pwq@bj'`''''''''/(=(=@B(;(:@Db2(8(7@BxEd^4T^3@(KI1Ixz* L2=G~Idt0?#$X WMЏ>G\~ǯ?0b:!.Њ1_\۟N^b?5 ǮgW*?;OdzG$nE@;"s}Ҙɜ a11OAHG+#;9(@(Gڌ0~x G؛0``#H͑</ҟ$隊_Wif*lwb/N͌Ͳs0NfEc3L?d?LxLw2@n/'M 4hXn"2@d K@'@L &\3;销@Vi&` D`)fM!̝4%p `{( q3.@ 6 Ee4i`ռbP`T#X6,#Fbk z@ 8M 0@ `~ P >' x f @H@'0$,8f6W];a}K%v-ۛ c4ؚwיfx 9@ 6lv0(1cZp0ZhfÀN9̝2P0$a0^O-Q#PXOIЀ@K&`D[fil@B@"H{XR) GD* }_C9Ϡ& ?/6$y _nm,ɠ&P nlgKhNd",VU ixO b`&K&d 435f+k:5P4M G-2Mld$D#NO_̴G@Rfii pT,Ȟ@N`'6ۀNJ $À GSp Tl< &V5viًhYK8Znn㻀PS͝B?p7=hfPQ"t_6Zp *3<e* `Q 5?m6`h L|Lg'@  zƐ`Tn`0)~V]`XV Mp9e@ Ioߞ}3fetJղd0 35S!lPsIi d`) 7)0o 0}0FćTRH~C%xT 26@M]lI&H'm@Pט` }`%YfBCG  0&i6lOM3@M9HNPZ  ͬԀE֝H@C8{;@O@l@sWO@'p ¡Q`0bI LCMT)x[@Iejdɓ5,@&ax* sLɩ@ ,:o7d m2c2NMP@c59fon,([@ LwkeD̑ S4Lkۖezcuh9MP3T }d6dзX@)Xo~d6@JI5(^k,fd|̶O'K<aZN`<ȳ$ H3̄]`z9 l\2U٢yf3\ɠm)֦$|Φkn#?*䟓G| N2<͠%Er @Y-4zbGϦ*=y4$ڟ%\TK=he@T) %*hi&?EbT`BOAQH1΃vvWw$s_Pϫ}z_;_L;#L#LP@ TEtp1 w5|ws"|GLgsB7޼(HF䯫` G>yD  W0`ǣ>D$_ɯ_WL`70pG B#{w??a(N' }0QZ`.E`b$Cqs%H QX BOA`킍]?SA]12x?&+G?U%)G8 G qBкI8EtŽpP(☀(%څ_H(W tl8V;B#Q‚_??*L@W*B+ #ף0aw ;(*Ex?w^3t` X+TAF `PCV@Ug$Qz0`H cV5u39N`1}Oƺ &3@L2R}Nf3UDR.P/ \@G(g\: ܠrJ sfE@Ng##:> B~8fsBܗں2rٕ sw˒t9˝Cput~\9sJq0`r˒t~i]M~wÍΎ>}KgX|,ӯf\(Q9ᣗ&t`f^h?I%SWG ^WgD D+:'x?@ ;}<4Q(@@ g(D=ZJxcG\qDHMG96QKvg7brÉ<Ày92ЙQ2> #-.~C1NT 5k7:XƮ4?|lfU@I43,epiO^084.rHq#y0s1ii:燁NNl`ldx\*:9zI<ypn0ks|)uˢ8Ç50x+PBʡa4?FAh ^4r}]nG.]rFʲ;2fp8xwfAá9DToA*!%z,::ʱ7Q@xֽ zЗ/GB^%JHrGA::t%KЗ/GB^ z:t%KЗ/GB^ z"З z:ЈB^З! zЈ t G/E +t"wA='ǽ^]W{j巶"ZX@/hGҊRCOkョ+{oh{ݶ԰Km Ӡz{H"Km{A{އKt^^%omE-J7TsһޝyqNw{w7>a""+㻻 сRۘ@"C!08RwGfO"x`Q:\ME[ۚ{1,^竱JT> |eP@ȣ䆰E{D^ B`(&@!AK$z'X*3*?݇B]$AEb A3NgULJU">VpcuBPhU\/j #6y`TsTQm*D)%@ٗSU1:J v׭CZwP֡TV<کR F: a(X,Ҥ`X=T805PZA]}TP/1\!z. WED9U@;eEP&2Wt:X{/I*'X!BIWTSRIJ(BP, nHV B|tBR  .- T^+o*Aq/%P l#>Z1R0""6 T(1HAws*,TP2ZE⠟@ Gq{*:9s<$(G Ry$%Jܗ:O_Y (fP4U6mM*1c *:""U5;lT֩Ө3sm+ 6Pq Z*A-(PGZDžEyPz@g~bf/'wJ℅EܹqTTЩS ,B .PaȉStTϮ8B*e p%@+Ш}Rz7خQ(.Pk#4&TST}Q 2LFAZwzSP2U@ʁ7pn&>sr;OD 0T(2IP5("MCP.ty<빚X]Q^iQ2gJԂ ӊ\!eRt*P96_jjbcP||C/CP$=@*| G{'}wDJV;BF90j v=:WA׌̪BxiԠA҃:S ʥJ(|"쏄FBWD>TT F5WJ'Lt⽲l+BmUF/QzƚL+CVV'm!SbWK4AP}Bڅ£5J mt?*7L:*eb/M0zJޅCXɸބ8R'CwĮ{{/lZJYJx ވqTf2wJXb"!]P~  uJTKd^^ER/<'tHt{yЗEB;bRCޡ "/pV^/K"/QzEEa@~z ]һt;[t,C؇Jһt舱 opP/mX)VE"" QC󻻻;㻹\q F7@BQF<܆b+YzZXzt=QpP /P(ޡB)V : /PcWKc{WlNJa-{6^ً"!ȲĮrC{=a!;t,BCb B"*BXVK "ī b ,BըejKЗ~{.Ύ~t^GB::/s{{GGEtt#/s#::::::/sa{E){ЈK{I9299$&,dhsJB\ Ipk[IMmmtrmm$ꪗI|$Vb4I`$ Y/oI!GfNdǜXq s1& hLd#$9KE*nR}) RI$s9O8P1_m)炩fUG4ꭶ}u;;k.}j{$ URgS^  Y9sI[o.Ӈi2R uόIi(pC2RbcJIU*VT] ,*XTaR¥KTmRڥKjPuR/`BL*U0aT©S L*U0aT©S L*U0aT©S L*U0aT©S KT}R*]TuR-[BK ,*XTYR**TTQ )%T= U&Rb*KT%{Ct;+Wt{Ν*~z{tz{t=tC&˽=퇽:Wt==7U`pWW+(-u[c%eB RKP(":!Ak-P>J pPW{_{b,`P8  #{6(:Jpȁ :JzSZd(A:NeDʆ@ Pnb @|J(z>naj:bU@Fj&;LRTz f$AHB:t]Fb(Ow0=Ww'ggRWOggSBVXt${cbU+[8~U 7Ts;tgUw{@֨(/Pϔ* Tp:WU:9<:MUQ"* TqQѵP)؊*Tm0PAO}*3T.J ̪.T9QP v8ʡGTGWRtg㫪JuB#P|oD:P>qPWÉ)b(K F)kU@gz QwfEAf N5ʟP!{v"EH)a(ޢD^@R@EB",^D" a F."꽬˴B){E, P7 "!7WPB(B!QzD^EEE""J(t4C(( P(D".#J#Db(BU *T%(JJ1BZ T&(OP (PJMB5 hPQBU T+(UB T,YPeBʅ *T,YPeBʅ *T,YPeBʅ *+WP]B*VPU *SP J((PPABr&LP)Bb HP)B2t(EPxZ'KJ$8O V:02ds a aLQ pax ia(в9,YA0*0qYa:C%+,%FəŊeHKD,3L3^5Q!QBEj,TXQbE*-T\QrF 1TdQ5Fj3TfQ5Fj3TfQ5Fj3TfQ5Fj3TfQF*U0T`QEʋU*,TXQZEJ**TRQBeDʉU*$TFQ ׵D[<7 -6KxJf2ϐˣ5G%vf35Bk,[3 J#% VZrֵ ^s]?Zl߀$]6Utī`M4b$%-xhuPa9BOj|_v=K–[algX~Sݝ6mc|YgL=1ei&L_M[oav"PxH%Hk\  7u" etߝ M:jEzj-{jCޒrH Y $[v%V t}^r[mŽ+/7MRRezZ YН(J8tn-Ѳ2sb1Udg%zb mzD`VYmӎh:%)2}\Dy%hI7:hRKnZe[wxYc-a u#ǽwd|[bSYbkYoLlrtXY bڈty俹=^Pj\ecpm)zzQxE/ C]ά׽ w.(pa{zA-Ѿɯk{lTTBQ %Dj#THQ*EDʉ*&TPQBEJU**TTQRE*,TXQbE*,TXQbE*,TXQbE+TVQZEJ**TPQBD*&TLQ"EDj#TBQ D U T<{Or-s-@";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;{;;;;;;;;;;;;;;;;;;;;;u~'KI{!'-2ȗ b?ڷۂssP\'0/X$5 ŊF+)>_ s ?#Y_\:>r&`3515G6G `DYLd~2̗Nvi4peK g08US,[o砓=s>L/ɺdٓ׋f ܁\=sO)1c#[bM-Ue/@Qæp:P|JSp #XY t:=G b0/$PX>"_OOﭿ/?߶RDԏG Yb$Wc2n8Hm)> r |0RpOߖg(sG=#.O m]{dZK sځDzA, =\.cTDI X'p"ྑGțqB^ffg.Y:64_^8W/bHW[sHPlȼ|#SqmK*A yz"]Qy YE&;B.dZ(~y'_o+|#zřw~B8HJv8Gxt:ü r1mrkҶ LLllllmMMM ߿8aj jzԶEZ(@YDf!QEւ ˾1LⰘNYإ[ %˗/޿rlڳK=WM;"ŗywwyLaS=2) ;iS"oOutjն.j*Fm-[KvړrC4&[rQ/ф,#_>' v3vN{U۷nL3&4ܻvۗ.m7:xi3)g>H8Z("ĬY:nHGQɰ"ϜA{r\"A3ѽ/Y}y~>P&<1VoD.9_y+M}G,qw|mcf-'ÌX0 4aGgrłOggS@BVXX\*> ҩAPWn,Z 曑q9]Aɒ_qO[,XKmsЭV;˺bd* ie|}#A?%bŋ,~,!Bŋ,XjK*XbʎX"ŕT*-EKYrKiۘ3bŋ 8,Xh,oU1z-,,m .]VxueªbPm$\?NIA"ŋ,Z bŋbŋ%,őh+]|!hC\+GBe,>XK,DXbŋqTVXbņX1e &~|B JfO(E~:0 *W$rXc 2Nvm?+6t}D]f?{sVQKdQ]Q ^y\WOb~ϞSW 5:H]B<3}9{XS5+ SyE'vVcUUœ?NU[VLV"BъV}A J+u-VD)yگRT]Z[;@g<G뛢`Ivͫ~2v|W8L֙cn ;1m.Z6! Zkخx>,dhěapqۋK._t}#t%RH)˴\1` .<rz>cq_ɰo=Ƹgid=ѱP1 1M`V>$w[58ù#2<Vhƪ·B&! sfsf'P!C&5SO|N.PC_1B}"N5axè\ o1g281'z&<||0xulЅ.1*U%VJY*UdU%VJY*UdU'eVJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdVJY*UdU%VJY*UdU%Y*UdU' xL& @!: ^" x@ƨbq@@&cF?8kuҤ,HBZa9Qqpv.*!Ž8ycu d;\/!~ CBz&G5T(wF'A8RQu^P\qhd@:'qQ"!hqC {u|v݁'/B8`D`n _ -LK~Br˗.]nps$˗.ŋ:ux/(xRJ`˂Q`^ 5G;K?6g]wn"eexXhwxr"XeX渊J **RJ9xd)zu(eJ7K*xp/m*1'qnq/'?=!H*EW_Mf7 ]EQGWݻ(;OZ{s ~ceEǽ!{@fzA EQEI&[h~ߐBջ;EO^w͇$9Jx<߼Z}c (@we%DpxsK_Oz|>f>9擘u$;7wmu<α; =g`=+Q[e;|Dz=C}Mzr쁚/By;{ +M:8iGH0@e_`KpS nDdfzU.&Fwu_Xwx ~Fp*SUBU X__lm6Z`jP-a7u=ߌ&₨j@M-,jbVSkv:pp;BU R'IRR%)2BehI>$'Jy'cr]NfRAӉ 8p)8u%$t.IwNRAԤ}>{݃5>vxx xh yn0F?aZl~·;u<\vhFֵ۱۷W(#I^OG}i?K3hyo]a!n#ȉKϔ&T j[O(5.ihA7q rѥٯ6չr]ueg]5W )\PɶiV-  [VP)ViZじͯ+r䬂/+!uf,`EDQWpZUh )6$zbStFx9NWwfϊTm'n>C62sD`VK^pZ鑮+@;UmZUbR0a86Ғfg(a AAD)QAM11Q);v QI]vpz}JFŔ9pRЙ,@iQ6ɱv܈'ֈ7.2:5⼢(W +OXd@LAY! EgQiUjbVσݮ( 3UT|v.3r{DT1?hv( ![Hrm_و|R6ݫs[WORgχEjuIj"|]9@ n%m2 @]T-J\2 Cksi Vy_'\ؕI7kjQPk/YEgӔq5{ThWN:[OAJU%jw4RX[J r$mjW]o[-nRmrne}nsgl*!X-]uZL\ջ%˅1+-P5{xK&gp+ǵۼ.!Z`o6v)Yڔg؄˭4 #kꩫ۰k캫͈-ej)^b5KA*|!-p?l A!q1[¹v"t~A)W4N+hڧ%Ik9S1ש:SrEB+ϙ-ARs0edJT\UԒWw7Q8*'FyMMeaei)_]5]|c_TW](!/4 j+7h)SP' D=K"qLAijAC4+MLjjE'LEXj&nmj40 0nuwWLaH]F5~nfb*7'k6h0QjCY[Xј"~R3pß?o:O)"C$[%<ȝnξb饲5雽z`|;ۚ5ELjjtũ߀~~ #[5*x"巁՝\q+rpyq_ngjqiZօ|q/}q Yq$Qp9xNtD5N〚ҫQ$Un8 :ojqnK:ejTΗzUי: /gEFt'yc^m~}ݙn;OwdJ7wko }YNOwd'K2܆{A# nmkؽimnQK9fp,&&\g//-dݬD5r7///7m'nMMw/R]4JY17/^S?44wKI$H$Kx.;)p > q>9l %#x5 ~1  \>D5')GCop'dg;G9S9>I:I3k_&?: Xqy&NR@FM }n.);TGBga|XA!9r Tw+~wD"b%Fho;!<аiy:8}=$Ggj |p$ɇ{[Jt;~o}NOg5!Fy5,v{)vH^)Oc,\urջ\vtZ&AmMlT.m6qSvv[Dֹf>oA:vƌ[MAL@"eA)"DHD@$E$@H($) @hyu>_{@{ ufmr5@#+/#]7Z\jPśe^\]L1xsRAs-n^H\nnf- Z[[Ij鳭6u*MYyt&Es&D}˚WVZCt]P5>ImJSBlmC+ .̺[m#嫰wn6݄+y*[pWnyr욀Nmy͂vmiPXfTDFWwW!@n-p33^^|F˄;v :sve"r+3RU+\Ifgԅ[fJ^dPϲ+kD jP1 $R'G.gcp6]-{˙reJ~?Y$EĒo|WN-tg!y8MOggSEBVX; ,Fwk_ttc4w''~:6d5yKJo$ɜ.k:_C许XPG)ʩp)ǣrO+>f qqN?_;N:z5uwB e&[ITp{vL;&Bj0@k mZyQ YK(@8}qɟwi1$o6B.$ UisƝÌo~a%rM#3UTw8?|֧g>3KV,բ>\0y3@ט}꡽IOYɓn:n!?xShLU3R #FW OJ @5[-pSޟχRxʾHNώ|s|=\<^.h/,!RCZ}NSP%J,@0儝w_Gp D gO>7^xaH68 p}Óu| e|y#G G)(M9<:r22='מ\3O &qטbyyq=^y1>x?X߁y>) r9I؞Oؑ}Dҙ.|2DGvSӇ!TչPIVϸ>D89 ؾx?< U0UY)IĖ@A &,(샄p$%@ !14}G`>x}׻v_x[>{h}}{;x#?7?b;@`60W"z>',TА*sOo, U%+n*չvl RFmrFՉT{v[X ]<0uzBHo]BgN"Yj.Q ՕxVt3iyM-yuNS--ցښE{W_7 قwV %@+s=-ٶV%#sSj2yɬ[Mmzٕ+~h_`[g {>(k6z59]rܩD5nqkۉХ!zJSg6We.!@!e p*lͿF 6eJ iv[?p=SZW+]%ε\<ܻ(ݭ(w`-;ϫdqm#fJ /3cYIP,V|vw~u@BFp s?~ۜH—F[m`6g9^^l n[_ [\.Tj. sZJؼ~;v0wj酺WRKuĽ͙=U-Q !s+e.8 Uhބ().f˶Ni}Ok˭Pe ?Ys1쿯;n1dr`ϟ<>RM~gH//?~x!!g'% =|$rO0J]V;39(clML™?>\#,)02{*@1yi}ҏ "~\paRdrǥZRfq/}X/!EagٜnjQ+l} T6 lD4%Da a 5HH.?Ӏw5#:ߙ_y#ŮjMg42F&#G+PgDuARSk)gc)+\žxrH}ncm;K[T(]UT3eNKPU3T)S.tM07j}=>} \+j'"yɊL6\<04g_oi 4[~|;J35Qi31yyOkcBblGxc@k:4qa$ ZI% "O@oo" S&?&zӞWN}m-5Ŀ\p~-7߂]x>x9vxGGw=EOD3|3}=wSѧ*|,Ejʑg'_ $>g;<~M=^sgϞhDasQ3#4y= ~aNqY$6!S>}Ny3C}== 3tóznVRt7uzSɦgay5 NsFiH U~$d8(p{|^bTD_;gJ%;9U91Xv/TNL4< tC!=9>{jUժJV{[|E}[nXs}q;w`wc0;~~F>~ 㟆9=DY/H!3B KHQ( 4hav}}}>x}㱣vǸc}>0zc}}Zhh orO'G9D@hqF]#~\=YjVu"R5-}.RMW+@B@RAoo =Uވmsnuȶe"ڳg̀\B]V63mYbdY[U.zuYfεuGM`^>=O]pU'[ٮb]%V*'KRon:7_+?Ldc6.]|(B [ַTT+\!Dlm|#g Z;B"&Lb+lpۋuj- Mnykv܉OggSGBVXrL`E1ߓJQiq n[7qq0")n]p ˨V$llB}km:¯mC`+(MVIB%6̵K^⾥ͮNM[W@Z04 ]Ůx[ /DH6[-]˵w1?)nuvLR3VxB_غjV&rRdJMWgZZ.]1h쵵&zں.B #tGP006a-ID; {.a UoB/T1ߓG3Y2%D:،; >c?l'S42,/|?zqaC$o hSeP*chYǷlwD// R Ό$!t,y{$34™w¿u2D/~Kru%x VIJ,~5i #:JNESwS "pիTZ<~z#=!e3*7}g&}O%a")<_;SKs;|; /U)/g>OݶֱSFk1ϩcӗ?8I'$\h =Ѧi&<8r 4wĽ}zxu=}}f<||||q|I׿wcy@}ܺckg[mݶIl"\KEp3ї VρZ|܈'c|Oo<_O;?~x||J|=ڣuWSUt6]օR.#;WkG}v895U\U\!Vbի{nPD*cJ5ѮxHI @C! "A $I D?!E4v ˋ~o2A y DW8VMBby//@2/56|(X(*κ=q2շP\ڬ+H dXuv[MZP7Jv…۫b5̓qX\2*7uڵDfvyV!w8RW[6w[+^gV?ԧg.gf[3v+s׺VbQ¯И\]{~_ml\Rl\RS]44DfR]a5xVJ^k^זߔQ{%DsZF`S7AC}Ɍr9V{\L1D0 UvnLM5[0b<J??rc~O7-LMN:9n9sJR\ rssmwOpsOUr˖MeC3vgX8%%Q {..Zv{۱ٹg9qcW~3;-ߒ4w[Lv-FPJR<埤\vvmNM,k#o`gS.R>~'NDzIxg3Jx?h]as6aḓT%%yȧ]/VRH+Io~H,Ѩ=gli,yAJU1.`U]ak$Lby4g:dGeY0f. f$5` ҍ.Y)@aXawV] S9mWX]`mܵ9wXe;BYo7$z'Z*װנd`;ro1^/kjJkh%f[B@^C;CNO>74m0 L C&F }Ew/<8pT.j\ڪUT]\&W2w:{ \[v(>AR;!U7UݴC.뻵UXck]GZkZ˰۹eW }΁A_$OggSIBVX "N$H@RH(xHH@ R "@$(E$a @&_ǔM~Y>g-8 +݈Ļ7v{'_2jyWjSZKlcAne*9M^]ssW%ٳ竸go-&|a)͛5l^bZRdwkة&O7semJE Hͷ*\;Kiz:&r ^OBC[@vJtBA|ڂ v@AnګeSOcsS]g =.ն2BܻU%-ѕWVΎ#t{wZW5G [Z /-]ϻ~=6!iQ>v%;-]͜Y&1Bj[u%fT7 HۏB )IHH^Q˖p\&ujjSyu#cm1_|Ws3|./й&5gt% }Sߋ?̣XN{~c sǞ寲sRvQ+V_"V$LkWũŔC*WzJRw5S\'6сZ5觷N%R~r-L':v&CZ1$ͯJl7u;j9m0pwzЙO}>*ݱtQaIԑOysZӻJR_/'{<>׸VNx 1r-rR@p>;LǥףcY;DŽO>G.;vxNVx><]/1^/q#GaȜ!;XSxϐ:)ؽo{'P~iy+o{zS'|~h~ΎÆaG%dӍ\#_zI2x x縂v8둏p؁bLcPtl(| =CͤxLLz=_BuϛF'{8nAψ3#$F&]8y0-z(&#KNRݠoo~xa؃PX&pU(\v* S] )~|DZȟBmmc|wsW!5@ITE swmHT}#][뵶mm_njߠwgfktJE;~+'[M ;EȎta|>}|.!̧ )HIԄ!!ӄJI_ D TI8q))):FD 8 HrI9R!v }G > ܺx(eG0&18\ -}W-@0V+wVmk+ej׷Uo2mΥuZReb1+sw54볬mZ_[o9ݳ#kmx_9IMvݣ|% 6ٛvBb4un3.kɪc6sv]whj(Nz͙WfdWyV15{ s6*x[Jjiouv+C՞T필+P0YC}S/C[@I)@ցUmi#{J 3XhAVpu^WEV%HWVnݵ1-7ӺjM66ک͝p@,s-ڇ(҄ oJOV%pJ{K~W]{I ײ/ jjPlfc15'piW`;&>A 4p6cmE&VZW&eJs.,SqҭblFmZZj̺-]o^źuo j9֥r ^O~R =D5L($L}1αc1I!2$Ի4ɪ|ZTsX}NΙ}s:ΝѝNog3$;:}owHH=ثȞ~|9;ޖMU?!!ͬ~czLpW{(r5ύ>G]}S%W/r#Ga8x?gF(,!,0@xϜz$S܈3Nbu@ 10 E4G:i}4S'N 'k9fQ5 iȕcق Ag&oZ1Jc܊8e)i1'h`p%>g .#QsLw9v<bb^i>g'&4| 84(N9NOA0@Ccy??7KJP*2X8S10PpyWQ>& r<6h{÷mӛo{cfvۆ9{jK]ݵGޓm9j:޶^4g'fkvۅ<$A!$(A HR@$@>k}ϽA>p? {v"_\H?kyYm%̷ YZ>-?_,m\vr:hUv{{9՜rlY+Q֫ U-#UͶ+-d.Ih _.یJgV rp0Ov|ǬW{9u _k3ߥn8-]-p ~gZ5.?mH^[3&d}OggSKBVX 'Q[l!i;ӢVfֻږU#!Imo&]&g]_Ƕ&W W?pu- V)KIZ1+$⟮_?*o ˊM1|bRXb[ZEԕ^d+D+ swVg'O BիOAyW1{\:9[#$y>\oSq1FY>vFr|H?˫5:YH~4:|2 a*N\ٵ3[<j-p}kY_Uj4(53CfFM,F%t^,*iBDFU#ץ[UwE~~ݧ~dr:+$ƖK*BVe5NsX"<~3K 0V,D80trH$ Qt"BKw{`H˱yF,_[Lv E# 0R:1^lVOUcn&DYU ,0Ė[7X现 `VϘ=)=ߙ^x:H,iCp/%$CxLJ 8aE.]ڀy%G%@>J!<AO T;f,2';a@<xU+0իVjUz.Z8֐cnVnzr9]qP=fxVC5Ubt7FU,`XR Ȉ @䂉C$D$r(D!$  `vCBvNyo3T0 ҊnrOMtje3D-Jmk2ޮΞR9^ ̷s?Ongf^ZdXέ_frgb{O0?ٙy(%r5NW+aʠuVo^(- ם.T\[r$iZ"ŷj;6PO] vi[\4. >b2glm` mhi`3>c{5/blP$v!Ӧ5vskkԪN2̀Z#ob[=ͳ+WK]J췯w<[Y܍MhF.5A׻w{Qm\qnZ+#^;Yq9G<x/YVSg)nedà w|ˮӤSJL`ee~{VSZNuQ9iM-nd˕@WS`3"˫`Ղ`q WVŬ(Čxcb5uG%кգ Qk&LaF N' 5Yu`Ա.\N"E].2ZPm=ء5>6 5B8m׏o G#$uʂUhqaa .((HH(@_a?|,yG9HAFk1*u -VM=4rSC[}t_>>Vn]35Ti_NLa#jt# yޙ&" ݄v>A=xB沼^. g92F:c]< 1;cjoO)x9.tͶC pNj'gcjcO#4 2XXs0<3ɦBM/mRAJ*o|C=_LMJ:h8$\u{BTx/"vutw@7}Jk!Y/rq j vz;tut2rguݟ*R\U)! WR5jX*kM-݀Z;2nbj;:fjí{V]'ko+L]n˗;\Zj]l G]ڭZ[x$tɇj/ s9X ZmRi'K]{돩<&mrE8H@$؜͸pC"]sj[0k_Ǣz]3HPK) JL0puskhPt jVOerh@J(|]E8͵ي908&˭Ք\0.f!i<eSA ֹg=!m׵n)V<Aj+VPuݯ0ED{2笩5v͗n\By^Ywjw C6u360Z2%e`. Rc`^K#nͯRIp[l#Wī+M&n)ovmL;'+<+%fh)9Dy\Q޵$ȕAsubhcn_V%I_pGD3Jqpd/{eٌ3azQx81A>Jp?$< ߛ_$|5.?;za1w Yrcŋx s$COggSMBVX Cq\QG9ԅщ%bXpd4swNB]dʵvMػ98O80lhq.\5aѻםG Txz&SrS\k{sVDveKf$!4&dK21\*ٳs(˕T򛩚3폆o[ d9NMc-v4reˎLlrN]=JHRջgO!͢e4B X~Uyo2DD Q.ɢinNZcR9l&Mud׉,0.R25fiJVNEchMr8ZFX` p[ɓ40a 4t_`z;o W)x0hÜa1? `Hw9d4Vrhw~FIx\3*')>I=vO+M ZOaFwrw͊{woI;+2wzΫv9SZn9{㴹fr(Voz%Ӫ_SX{\aEHT\36t8KȼPeӼq8oon޼ךl܇ES^w4Gq>&zqz#Gqw`;}o_S^ws}(?Bfc]ݛ]Yg2tv7tI%ݹI-` @pN gßDy\ϑT#qr^/ x<^/D^/C/<Q#|:szYN|<磎xS rtt9O$=4òyRu%t7q|D;?Ky{/sv33|3hr5"}5HW"ix Qgb}FR D`C0g!0x (C<^ftY< *{o{mommN{n=+].w]'um|!)})V@)m_}K'IJ>p8u\ߚ:&`m0@%@dCX`Ł,a*&DИ4 @P(0@L!ŁD1`Ĥ` L}4hhxcx >lk7W&l@Ҁ44/-~~!.c}>" #5|[al6x$Qj1y hg̺{!ugx~#>[E. 3=Y DF?= AiE\ϋ0JH@y:>щZl~D*O{}''8k"~߯ W| W>v|D9$~|N? qys**Fpn?\lyuPP*?ϛ@`>]@Ldd|{0^g<\0s<?ru!dV!C(2 H ?ȳ.V~d!uA^!p+Ht?PYLi964 p_~Pdm"2^s.3~}g~֘j2એ>e2yegG~NC^:!1co?9ss͙h[I嶌7wFfdD:rzLNpԐaT[$9*/Rc@$ĩoUDd51ⓕ{ bc;#R߭KyxQN/8,ku_=ϙy#I؉U~rvw֛(3Nm S(>`q>ϴ<gyǻy!O\ywޡ9G: H/vdsCۧ/s짃<ö WziS:gSe:#T^OBLKyGC|IZkLqa^oU(beohR?rty},1c8hcB A燮,~?uSg9>7o^Ǟ;orgu8 c.'IhOM:B~V>02*.,<U\~CRO70='O!ک2rSZO|=|vwy|:=]~k~mw~{}mߪ5EuOggSNBVX R#_[]uUWu~o{vm_~n߷n~ݿ_UQ.]wWmWZ,{m>J I/R& V(+ Y{XW(DتI\lU+X5V+p0䅁dX+¡Ul,V \`d,V F`X.6 ,, %7 pX .$VcU 0`0Ba\,+=pX*+@ PP0ht4PMpkրd<>2 gkSG5[?9K~  44NWdG9k|Ӈ5#3$g ,;d^x,yܻ9dkxFBJVx82$TZ11u˲m Dh>]*ˡ:M`S˱95g\4֮OIm|i}-ʗ!:o6kkrl]e!*Umu2X G{ZEIˋɹvmvfI`;85;ʝb1]ݯi9KUmT8'qG{j휵Ӭ%BjmH]vb,1J&zt1#s"uKʢS,QjfۇĠ]YClFҤ[/3܏W+O2@s$]YJH1M-IV{i5k:y7Re&q ]j|RO[tRTymEduؤjJD2/2JɮmG V`}@O8}%~K?5؈k*񠟄?w2@vEqrckH g͟v,u-"".!5OdW8ݶ%/%TXQn=@1 o"zϸXAj],T4Z"ԊRR6DKQO ږ@aYTҊB6.+Ninfնtڑʓ=#{΀G[13l\ĬGu@}BjUū@@Bʷyw #jN[.oPG>+RPY&r\5rm]޳݊!RuqjB)'f#r\.ggO)k ur]=#+BnJ_i>K-Vj̒Dle]iH|vPjƁU[<.wKT"P}Wyr۔2UcZ.)K@*~o-R@ '||G-\ m]^9ZQ\4mu˼ˆy|d )%.ѦhxPZls}ej.yvMBo39@Ƀ~7 wA7?d }eiLsLӦFN{7%E߰i0Ϝc1lƇp8p<1G pw˂~!fC AwG|ϝ׿Ñgݿ=|}cJ2z>0؟I#k"WEvб/CwPDže i}{ʹND\zأ%RqN~ p[p:v9QHlU?a+WqhoBE+f82ҏG2LWͫ]σl6x#Ȼ}5yGxɵTDO˞(`=F{.I@('0Ɵ|J w$zߛ >fzlr(jҕdvP0Tuﵦ-u ߡ$-*; gH$h^H=NK,ŕ ZTתUA8|ڙh˧DթJx u?kmv_AO3"IZ%\|#+"*S^Ofe߈kլ2LXp!N[0,/#ٟޝI,E|BN.3C"ge2d㉬]]c0k0*M qÌ&8cubUR5ɓUzVi]YY-.Z :Buuf7vBZn6ʬVMS 5[0u[.]ZEyrART ]dXa:0 I>]TAn}\P_~];((!M&H}3_aIms_]i߸I_\g]`[P.LP?AՄLoN Lґ](Q+_qr i%9?S,%SLnOV"562s2MZg"tt|ަe9gSc߷ҟGUT}?{5Cj2w~2(;g6lٶU}Uo߿n)Ns aַ%U^љ1ӭsoNvmק9Ιm7|umka/׭N{In:LV/6(<O}Iqh<^1w@۠3}z`yGՙ]oF'NsI{;d1$3&=G_uq@w{l$F*L,gZNdv??:rw2jNsSq1[y`՞uw4=Qu^z_Z^׳wu tek#X/Skk=5Gwxu f5/H!K$ۍ6F3;漗.ڙi~te3pC/3QXv $y=>Gtv r}<"`p;~$qyp$:%iN^G<#:!D}O$RU*xO\S{i8ϧ}3} VH'>o=_4WtϓOggSOBVX ĄƄ)GSw&'cSpwzCi'hk'Sx¡"! b#¾g"X D  `p8[a?`s'NgI [`tp{4J *$1zH0*@pؿ3mm}o{m۶qăo{}u~۹T<7~_)R#@=㟪aM"*?w@qJA:SF=9} nDD#0A$^lzH !@ q`:}4}>>>ڽŸxߠ}G>p( !]nrQ yOW CCch}bCQ F{ Zo7m <ڹqa4\O@gReM&epˮwV#\[yv!wfx\{Beˬ]'ٮ%rw/V{nDJF3gfT`-g{Hn>r܎݂e^;Zi/q5t)=Xue%g, Z_]D͗U8Kz[ۭ#aݰ)257mvozb(8gIs\v=~[Yw^.կm&-i7fngL@@B݄3T. X[F@>8]jֳEw`(϶[7Iui,-mR]l|-k`=kkݘ-ۮ0,)*VܵQ>)rۢgܵK}+@x?- _ =Yn F wudj:w@|ZAϳmr\$%Lv \VE{6:Kƌ jʹeK"{'֭5Wֻ=kʯ[bWfd }XC9hUvuв"3-݆lJڐ!K0?gVJNpd *]~"sK]Ӆms\BXSUxQ!n s &K&mĆCYm=i xĘ Ɇ4͝4A4v- wS彟 LdY??'iߎIݼ37x}%yGhQgOO9'#~|ɓ3/I!- 0@HI(&Mq'I2R6 wpφ <@f%I&tv&cc'K8 -zRȅ;zUt8]y6g_x>*#}9^dȨVB_]- q 8>Os5?/8{%!B[pSM"L)B{"}%?UNG['NR!Ҳ4w9A!*؞k*&^'etW) 8OPg+ױv ~ywiSE"fvs 7ӲU9n
    t̽ 3%ԢmwHݦ j%]rY2Jp6)?fBɩ󗁳6Vn\Ub0PˬG+ ɬm129s-]Kҿg *KN;p\ȝ|-IS䕓2\~':QAk /^._? *uxS?=klKy$Dm 6\W7meL4jםV[W,QՅ:DXB/4.i؃74$׿rNm$5pg0TI}IjA[[K#L[i.us?M ԏч,lCI:@]ʕ]ctQ-2wFt!t}N' jl!pXd wpcxaJX1WXنp.WVaΎ!Z&_,5lNtf8'402Y0 nֱawee58eҗѩ T%гN][u8un6k #ib;[[Q668k.v.0 uubqx,r, p 4m0JteUҺ9rŞKgI;_|>݃ [l?m=}?}Y/+"c\:<:~|@s:a߶k(ήG Fu o@$M0Pڏ2~90 ?10p )$gt?mkI 8>hGt+uZŇyX%m@gij KVͭc3ƈcbv$̋L:ڤ,%.0DSϓ"`392Jy~RJ^;ݞΞ蹃ݼLOeELߟS ѝ7:QJK_S;NvBLhwo?7ݿ/rJk%_Z}Y0't:NZ%o|=%m fkk;4nwɦB^>=n< qS=; 4fhi7 I%Ngp3I>A$Gg|Y O=I$r|XLW<_Spir'A;8{zCv>?^߂9ڛ>xTpɐКI](HW J1@bXxÞRtC'@%ڻ,'x:<_+dVP'%aMidO0Dƈ˨#&g1#ИxQkÛ[^yyD8/GpiM>gvC|F{!K'}ޏȝiz`D  hZi{xYi˴x |omg {,f׷Ȑ؜9 SIu$8|==s3|_>行^URW[n<)/Ypi1Drgk%܇aRMdӦLԽs~T~:8`f'`(uz909oRItLCr !Fktq~Ie[7$" PPR2nݼa~|'6Yv欺XO B8&1ɫ({dh)) su,ʰck 1fN"Z%`)cuu7X,Y#CSB.%c:5*֖ 1dfVT;u2 ;# !,XA.f03`+y3)Wg]OggSRBVX?T+mEiUYP1ϲ/w.=wL=X ˫#$Գ'|0wO__Ssڽ~wI,NHNp|s$WTyJ |S;w5?>7Wǵc~ Ny>'7o[tz~so1o[T6 ~]?c[Ngw2440X*bQ.z1K ?i̲ r_N{>/a<` :΀l]w\S o؇$/ 35f6lI688ˆÀwmt&>J"?.A9I% p@#((JD à@}r}@>>4 4}5NF4h_?`[?7;[2GGPBClf($ o?ͬyf_DϳL̄}i&Ou?Gۡk#RH3]br7?!V'gX3iq9(s2 ?~ 0"kLB eɥ-DoAc8~OߑߓϗvP lO`@Yi(=&8@YT`&*H$VP8(넕GU]sIF&_M;aϙR>Lg3TtfCS^0,)|+[_옗^rJ(Ytvp'>!ô+X|/VvQ#և*d):x^/H9Wes{>OOk'a>6 E-b=>GERHdBLSxO`.N1<⺞\O9 )[gvO3o;'Ri|?s÷懇V NBv{J?qt"y;;|I$<7r CNR`!pdx&Oa`SvC/{}E ŵPsͳ0Mw[\wfs'LOs; Zt 0v]b-EbB݌4]n}ǽ`?PI/j$$BAH ZPH G@DHD\) Z}>z ~>@ x >}7BZݻv@t֓5܎߁SH'gRw˂ͩq[smqjٴR^ue͙\֋"Z'SڦlӖ\횕F| I=&g]&嵝slLٶrl]ݶvj8_1q6zy&|omke+ԋ՝ǝZ-gRțRTj>Ih"x"jt0lBn&- |Mz<짻8s7UH`.{{f-YY6 r<}[dg.0@|?l.a("_M/ C =e^i~2EI UsMh PĞvV|lۨJjOIZHC$zJ@e%ݐJTu`/5>v^Rjw_Ͱ `woQ g#f֭MN= UvQųw^&z-uk@۸W(`+sgj kI|zZҮړ˞ $ p/5+[Gsi:;4EE=颁D","; {Ya^TCu=quT΅')C6gYOg%x:oӝL?x}ap9Cնΰ8c!g8cT1̰0vchبt-}1QWr2V!vS5'<3{:VN%p~8flˆ5@)ԋFLee(əaGOQ(*E%ת|f۽vByv¨~\?Xu\Ъͧ[MVjSϵ;/̌-,2x,wS')*{Utማ6p+1gu'pa BQGqвZ1LL#GFE*%> TU 8e7WHc54aFS"kIZu wT'G r@A@(NYnXg ;(ҬvqWr_#J6% ]7!(YAⲅjgG.>$TN7°&G?4M 5R}]>;k# x8\UT,4'~ Ar/Sq*L=XslݽfֻnFiގWM_77ۿݩ{{onB:Ӡp><\l_&I >Bvγ'cɓճc뽲oź<^.UxNUx'ML:` Tu ty<r s}Nvy4վ"_tTx#:47G!νyaO%{9i=qgp98{D+JP< &1(3osND "g4@32^<4LC Ha>Sؚ|W>7bi2R=?y4gb"vc:GD@` u, R1 y3cȅ 0Og7SNv/ =pc㽄 )\:<ӣ h8U8  'b@ն~kkV~֒u_/=< yyN;~opqppqà/G98Cw:7SwwOggSTBVXRnSFۏ \E"BJk@-4 QXPH?Ƶe4h}>Nx(>ov>}ph>p hxg`B n}=  3#l'̺Z4LH!R.:Zݭ, -פB/eV˱z_.f˳UR;W7n\.bZ,j! $6b)nE .vnǾ'iR? }9(K.XuDUZ܍m+MݬWs-6aere6Pv*(m)t ps"\AU.v-{Z}#T}(_lₚ<wܟ&KVkD=ׂx @(͛.rV !wV颷-9.z}h%u{MuT7 z +Wlf\?9k=ٮ*nW[(~9@zY{5sv7f7<5?P %V+ZYp!-w5mqeP+ݵ [U[tk>ܲ$˔,׻Z  +K8|R^+lE*bNku][j[Ԑ2\>O_+.OmjxuE[!xaHH:(Q/u8۽yQ;lu#3e{!tprŖ/.*)?iȴbRj6n"Hځv?]\'Ӂc+6a$ -9N#k""{)tefC c_Vnvtrҕ%mCER^_4ԵcJo[xT Zuw{-gme8j޳8< zԚ;,ߺNuDf|0C VLgI]yo&bu=T+Ms}y=f55&r'̷sIS#>kW4d~XgR֚6NJ]{S.-c[>B9p#lT&de=YeׇnŽ4qyQb.)Ouȑ#iB[%)v--YtH]bV0.;XZwZ,&Y"yX -'@HqH 1Y0Yuubn8ݲC 8a\' ZV]g8F@YuuZaɐYut7(p:,d.=<0a0{q]egKf3XkXIM[bq8u5 l[)|}r4PVv2ePgbg(,]+8K: |B 09uL],9w6eoAx;cX9z?V*RSϛ9'5.sJ0욦{:w¹~[ {ΗN6wv}T='X͏~u'za4M!9{oHQz4w2Ys] 44ޖMԚkS3b{7r3kkE%ƃ=~qwz__N^W_x;S;yyyy{{_)OM|wwt滻5l:r\pύߑbNQH:xͲv1>G~iN,x^x`r^)OĩT@D'r}NJȂ)'|,yPq>{4@DZӀBy(_SM=,;C'!;0OW=HW_믆 @AH&,>,^m=۟-:t1 #玿SͧmեG,FeF/ngqe͟sn'w12~LLe3pWyw4QC:s~O~d,0>ߙQCzhA9,?>+@s&˟q~.o,@cS5"}G8(XRy]8bw$H3՜SЩ$]L}~C>H6A596y\/`][? i?oG]u|Y:9#gA%=u?F%QkGE4oQȣ+ N$?Ӛrc=?T}} (F_J!>uQwZ(}ƈ=}-"b+NL_nqEV?j0+氳f#S_I'=,}#pO޸]KQ^ldX7zR>BG=Ր^D:{Ż,n^ۯ+$'N co{m |nNw.k?ohԅgѷ+U}k=o%Z_rvՂJIBI&g>fr>A^EFy\+[)Mm 8\zGe !q[vrlp3|7χ=,Q'ruvbmύY=9W3k^.V:x~ /e<Ca:ouXSOISШb*WR} 0:>/πMx(11 ׊A9P4J+ _CuД rhWRWO5,:kOE3u@Vwx{~GS*)9j5J@@Nf|_am^m8h {HX=l, >X6 Naξt80Vw7>tmuST@>h,>F榆,?B?p4O=A/b:|$ O 674R IذOfp;RFUKQ7/8nsʯ=wmnWٿ~^߷Ƕnݦ/=wǷv͟|cP2?zA H(%$hE>變("R ƀ ` ϹkA}кF at 4w&^lV[6rx[!|̴hKjo aE+r)uіE%.dRͱLJŲ'{.]W\ͷjUwlc튻-M.kRVٻbT#6̯Wg]\[;Jtү+A D;`1+v RKm >^S^V5^jEsj׽lbW -~IsmrלV\}<ݢCM"9^fp,]`-׷T>kիF%[4/? CHeWmY޺WZκfmr t[΁zRZ.s1rT?3,-QNI૦B͝^j9UG|bZѣ?OggSVBVX JS65 jF]s:ax!8MGfLLNZ,Zg'S_žPO)Wz)𯷹^"xzH# JQ)9;v4?btpKXl:G" v{ =p˼G|s^M%L<Nd)1:_\2>/<!Bo|rG Ϙ^oo%{>'٤{ ~|N"rѨ|{ :dH#}bqxB<. O|OaCh TC< :{4Fz<S>D|ϑ—UUUUU絛U{]unv;gDçvA3͟]h ]1 c5:9aX`xvN-z{JQJ Bb$ D!$J A @/`xGZg4~1+~/0(]kDr\0WO{( jA$But [[: MR\fL˴J}HՖT4yir{ͳ][3k[UW<}u̶[m/V#bkU..+J}Um@zu]T2;b [.\G(KizOWbxomHTe!XrgjF{u&˟7;{ݿZKV.Dj޽lpI\28YEyv߶.e~mͨ9R=qքMCTmA'VmOVR I I]⾅Mٕ*wt e+g +6ܲtmu}R D5]|q0R+ݪ۵m5\]VWBK_.> jʟ%=R91$=AT˘D@B&pL"'ǦA&DһЩL/;_tMY`X d#nMCsy?ϲԵ3u9%#V6Dw#Fzț=M9pp -aӁ!o$2dG{RpߎN]2x'FnjWNk"j"|Սm#!;&dS&!&L2S ښ˗"U\ں* F3Hf%=]Lwn>0&绌e٠s_JP)]1m`':\7Lyʦyjvxo R ʉ aײg2lor\;O8˨tS\{-dz@䍎,A[sOY rwEIsvЪsp<'tu:^(}+peu|8=#9>xt]s(Z-tŪzxzSE7K;M)]..<˥]yω,^O悴VS 9瀟ڒEE}C﫯3CZ)z﹧E}]W}q-_o/wf:bS85n7e_JVsYsMOL%óg YL)yq^Xr؛룒ƺ_G3U2매4NC)E2yUںrgw7F}kZk6Oi沛ˈKRӕ: Q.In0FJ˫' I!q#/a .D֝@ִiv pO>_DGi>G1_ IGc'|O$Lx^/+~~"m}sYO~ʨuϓDVyH_< A8YBMi|+t@3Z~s0x'fO't}ޒtxU89 NÏ\vbv|`h{ D@g7p6zMQ^xa-G)>FFv&RM As 3[m7m^3mpm:t `}t9^1(K*+Ͽ~/wcpH1=%sN".;?a}#T3,aUA#ń ,bbhfbXIj3" @>Wޠ}>4}(`=ݴ{գ7=>x}}vG#)s.~ kGchn@`?)߁ULF,&4 ۇO)tP:" !B'2&u+2!,5O<~?~Ӗ`h_BF5O񵟙)dG>L3:Ͳ<{^*gs/٘G&fYVd x? Sn?8*"|H>r9$~t~U^((~L) @Cֿ7ndNS_D.fieG1]}$O֤LO]M{Ѿy\v?x,Vu0ě3L~R޺-ןߑS >d911͟dGb_P`roȀ蓯<]\?Q#eHb'`f =7]cn9jme1glM8-¼{%~DK@[ey\B3;N(d(%'. sAu>+rY(4Ý}Iݓ,0K<>s9q[x[f) D |'u0<7~5y^&YFY2zBCƹ_spڤ]M|Nsij9P0-CcG`J)W[JNOggSXBVXY S|ģy=ƒ(&5x?,Sw4*ZÜ؎uƁMz iW='.3siy<靉Sg{S$%41[dt@vXΉummef 0di^TRwa4]Bjy&=9^<)G*lNN@0>~\фªQuz  v<ش?xf2f-ZP1nZzrv[^|w]UJoPN*~^J"~) IRz=V(oIfn*݇Gr^Qw68m?_<6D>{E].Id$TO!+ѽ)@@j9Ъ1t]Ry95]PPr$״SjЕЯg֫1C4|]z7?>mwǞ.]狯WMuwx<;Bj.z=%G_o̼|?~oeJp!qqVrpϚ535^|~|@ooOom5"D33"D}{D˃ p>,=5S1]8IGn#|>?|^'*xQeĬpXw-|2|Z`\{2x197iyS !Vof N X^`+󽭣 IכIt74tPz;|0CbR1|@"o= OGo ”GW '4؞Ouz%`*y|OghopAN$o D^B  Y M mD8A/1 _mmm~oyoz*T״~m?D\}ˇ¾uP&8Eʂyq~ozD>r9ZJgw~1}s~m4jhbԠF, {QD1r0*4hnA bZ CDAcbx1e F`ĴCAbAcZ , aTq Bq4@+@  }>x!>8;OhO Z>A W 2TNy^ܹY2Eo5egv vV2X}k;m6;=|4mt ܙ-˪i&Jv "2R-inM8x!"-lCw*D]mJu`k0g˛P%,gq*ZYIh+fvٛjRPݭ[i+/\x;jUr;j{*g^6ש\ !Bo6f1rfPo( moj]\;UnMIRWp;j.v^oXSjR6C{T-]q<)/7TVͫm]o+/pKW3P{O(!Cm{ w<&e˒LF?YH;fp䭶MHE6{6]v*;ىcNsS f.׻\mjkEWmeppg ж'+"|B6r•?E6ҋVeK&H*E[\.ٗykU77.>W+QY+ͱ^W../*uc \ d,H )%(.]G2ٮʺR[ !k+:Ʈ罌s9] ??3 #dvgQ)y'39 tg9/\˟.1:=9^y=$'I$PdJ)'̄cx_<g(ۿF<.~"j5ϡ#<йyxh!0sc|e [.u :6~C"::*pIe.Z_> ~<ʩq ƣKE[٥p]CA4 `MdfRBzڵ!w2S.]c7@>x%!fEFdC5h mdn=|ɗ*9PjR]i }}FրjWBӫʩJ})i6C&_uǧ*?KD6ܶ?:;O0y$(0A%ITl7{)b% qO&V]u˨nzT;ز# thR oFR6f&{8}|UW[)>].Ss4].K4.SŊRW4MOSuVZG4=Y3sNu%<1hGSj ϚWAz;u8EV-g5P;:vv=c:Y| ]Q|d}jQصf[ݬukNnh !7zTJn;TC]~UCRUW*EU ]ELˬ*]*Te*:ϫUҢdttlN'C-n߼bk9̈.heִmhb]c ֮Kt0wvt:̈́:q7S}ˤ ߡ%_Qs*17Q,2E z B)B''nQO }Ϲ~s8\@ \NNi޴|%RK1a :p~oHLjg #N0`ì8MjG?:LY:Iē?%yyyyyyyyyyog~_;?gwD̞Jt|?Ǿy9Oh<<<<9<<;[/oo4"Mqz5ro*t"ri|}LYi939ybYWg*;ڽ^g%6ofлçty{9+}U^^^^^Sj[[mm}UZOggSBVX}zUUuvw?=ݾst>zv}ֻkh$SF4;4Z  )eK _x'`vᝀ7h{W}鴒+x(CC@{}g}󰫢)yKZtPx4k@ uK ZI.@}})xs#dGVm4н>{kUAxHm&>@c_z4T @`mk@ִhUA@Q]hhF@v5A] PP} AAF j h4Uv 5@@G4vhFѠh (h4@Q>((+@Fh@8γ:γ:ίw;-E+>,?}W:g'g;fg!7{?y ?巫9gY8gYγ:γoyej'H? x8ǖ> }kd?Ao&Go_ggRγ:γ:γ}F>{}{xv{ }/UHWRbPh%5VsQBSQ HDV&KD*(J:% VBSQ(JmJ BU@h%:%Z%Cei*BAZ8mLJ"+L̑ TrZuUV%8 ŷ>Hf.79dJOJDKئ\Vh9䘖 Uzqgd'x#Yaųɋاd6\_F*vAbY {8o>׿/T3|r2 ~on #|A]g#y~0s ~O[ 0 ,%t0&s}gxEIMY5cvy%FiØmg"Fz%d eD.ɍAqZ%OBjLHq"T]@5ÊP@r+A.xSΟ9W~wF(:;8*|xϳ)FGB o  "O\F8k xaky111U{=|DŊSG/ط|C p;ݱ9'GEq 5Be1D#\ʼnd#5NNOJV cXZŚr_D`Q3Gҿ|?~Ldžz}ĤFPT`=y>o_/e?~_)?kpJ_4:x(G)₎zO%`y5cU{=^g/a>I1sZy7qPjPéBЅ| + UT(%O V;sӜ |Bo/#^fpxGoπ 6D}8 `L󋿧NkA,ZO鄛p50':'x} `,/W,x~N#`:.i[d{={1yË}kd@bb6Ɍyy:n&hR3B\F%u!B"ebXd#݇ "vy!'PC$x)0LQWܰ9 - &pׯ(.3,Z )#y'w.2t揟Q: G_3A_D3}?D%iLVCfUN#{yrI1CIG1{<%zb^ؗ- i19QD|!nňJe9lCK*P ggA"yqW)g4#9"gqrT_95>z )L(h|}x1yi'^' G+ÙBWbqCHNӽ9aÇ1K!$w*xؽ2!U pV9W0ȏaf</;'\QRw>/lB1V,V9ZY1kVI[!o@&Z0] #an ~_7>+ a3Ω 2EVIf)f/XtEsx%Z>Cl+F$Jpzɉ[TPOش ŋ^Y2Y:,SSO6u3XIXrJMQ^Nw4E9;(W)$g4r,9'g x]7V .߳b(~xQhp5ž5SDOBGxt{1bO!-t业☇TjxjyD)rJ}XxO8QBV\5\mY+EM++O=e^-qE眞OEEe'-1)d6gkn%Fj`rb('tSŋK_F!GC=>W H+Jqg)%hA%1911'b,(++; *EGi(WINY3DK?/ w.E 4swO/Q&>>;W9sybDKUFUa!bVe7K[q#Z#X>B1$MNreuclT&S)Ƙ 92lZJP5RbR3UU Bfu7LCD x[묭uRнbI"II,\U%LN[M[ZLfSZ-SG>戉WùU̩p0Hp]6ɢA0Qdۅp5AO`ha{q]EV0Vy6G:A<DkA|_X4DOߧ9OԫNmF;e!7}ZU>@?xL $B+486Ao 4{`!N(A'2ݾ%QFMQpheOA~ק@ I8᣾U^9ހ@ZC=ػtRyUQ_+?V"uZqoAn<̖!Lb.T{bwލ >l{wbo+ɻq63,J+5x+yR珯% [ /]-^NqxrA/AwYAϞsamkr7Msz{qS:F+5|C d7a)ڛ?Op a}\+pK;ȻJ7xޝ+kwܖE4׷XjԸ^E%.fP\ogy?zþ|u/n֝s/G(`f#xc8c-V&j7bcC<ڎT2 }Ġ}!S?nu,a\& M ]{{#17nzՖx9Y?x75Lqn {Ǽ hкK{:n<1Kzv".˟)k".Hi?.w77x YҼ\ n/!.\1OֽupDn?pߧ. o4/2M0+kҽwFuaEoZxFڎ?-FAHgF+دi %] @8}Wp*Y}D{giIF ukżWr$7ޏ+pr#] oc|0;w;C›j.bet _wy( ծh{Ub_-\E#ݿw]ͽڞ ͦaE2/jx+ G]^pyo^R-/iy.WW;ƪv]i, x_sMWr7F1AxaVbCd֬)nH0?w ?g؝ ZhJW'ab.GoVheY5]Mpv0&Rey=G")Z7Na{AڔzSo.~39M鿃*Scwq.X`  ]ij҃NַjI']*on E77/{H/u{NM~߉~|&M4,n c2 H(?c/Sy`qr7#o1 /Nȼœ,*bڂ‡])ޮ}0c>e !zݧgC>5Z)+]|RkX~TV/$EA]'E7 57E.R~SַE(F?0#01 @2\wn7d RǾE7aJoV܊{;11w}aM@ܓ"7%rHCrbg|R7|\!0bD140Z}P0˭ɮq/sxrq|!w<)- nVSjmsrWzW׮+fy ݎ -JZٺ0 ξuNlP7sa+6nEų[ru,/ J"Nk{StFHcNhxvoyҲDdǁ>B,x#LIW 9oG嚣DgQ+p,m~颍?<u_Fz_@ɹr-plZzqrq ;Z`?eojc)O+_Y ^_Yo*UZӖ/zHG ?wAwE}:|6:lgq/vD6!4?YJR8qlhyxq`[.}.w{\q9(yyܱQ?2o/9bW >x{`OXX˥;W.͑ "\C72 .PCEOl]]Y˻_=%;"P:߄çseލNŻ(E.1xymز+ⰼ7ܯ al]|ok nn ;{z< nKj]D../o]^$&o/k߸B»3=›ۋ$|3~.qg ϼ2 Cx ki\=\ z?Vut]e}F\jZ\!]6".J5ezzvrCm07bz޵Wp0.H0ot]cqt >xC .ျצT֭GqKѿ-lhi4LOL^T{f^+/:Mg:q,u9ެsls:& BS_sduPCp/ zqYkuOggSYBVX.p=@SdaI)y'&Fs+fŞnRKɊq 6s6Y HlS$$<{ӋN AVM.D&68&3/|Q VN@}:j<u!mIFTQ"D"1@"""DDDDDb@.BdUZַ sgq;JxH@)S!)%$u@*r@%]H=EVz5YrqTjMkrɠ%x(QMzX,ө M&W$ VI>qW")rU#)ZхII&?h0) sQB %&ڗ4à<}ʣI# NvLE=I$+s´04ƀ(!)R*qD*֍Z B0ﷺ1Vuɠ QM@ u_rZfAJSe UUBER[z##zVQPI$o?|"B?S=XkLˬ 9>eJTҗYlRJȧsc?hpa.h(\zI7{ćEjhX P\@KU)qd`@d*(h0`։P0.I^a`1K!ֿ}>B~a޳Z AP(y17 r $x=$1:;ޠ+7#_ V0uC׭DhkZYwoxuaߘk[n2?<;F[\:tI7uCD[g>억t84{rǣlX(9777Xvˬq'xSۯ神<ˌ;cxy-XnW>\͇;t|gYg.N;|fyx;w%ۍY1o;G7F<%kKWXiju~ƾ:fFtI|ʃW?SAr O1dF}+l__\]n6hs{ Y]ϿRrEcm5Y6F3_3ۭŬ~!:s澌7߶<}:ntǮcc۳,? W2篲v~өisWkx;4MtzͶ6iIoSeW3d.3_ӌx ֟mȟ%ٵon̼U9Eml1lOK>G%ey.^Ox4#$xx)%w뱗(pvzۦ|>/]Do)s'ox ysVe$%RDXWʷ.;!5IR&'XL1?C%ٰ|G+EŻD,/CK`qJ! I%rxZI}RծiZM$cBWmi+I+Mw0ffZJեG6tpy4xQ\KQGg$do: v2R<"G.8ǃr5+prvWy3|ՇY:}-t9ʉ^Oy`91#'adS@ =Ѐ Phh@u"@Iɓ}Jñd6p2ZXCXfb #>Oܧա|t9 ~. g&rA1^T{;/h?aC8xb@x-<9 ,Bt|qB2A{n?L74y4(ٺ1rx9C 6|fVBu|N'*Z+#a\# frRa8f:ښ %NHUΈŘGф=r[1OBqN||)|{=!<V'y $wO+*xhf•*rz)>s<9k8<Ȼx~YlK}WVW'';>O`o/Ђ''))j_ab>#48? (E DUUUJm*mmU\UR*ofjVNVuUU[Ujj5ݷ;:vsν㴪Uoڶfmvu*msח!\Vmެ{U *UZ6{巺mڪڪـ1w7y;իZn`*.m-:h ; ͚Zբ JDma4|;5P1'8mmj6Zm( Q=-;͑)VQTUkjkmMbP(RJ (EJ$ B j Dq@%I^e#ݽ " )" @p"Ic$D)%/~ %BD.>=D;F|`߽_z(=`]l3xhm{s ;(x ` 4h7Z 6> .zxN9F)$vz54|2*u:$e-}@}t}] 9'B'+1gO)>=ɇ'ƎәI>&0fCtLhRFw~S>$jh Pql:xb)1z [D=;'.IKr$\)YRI,(ӥ. ۽]u^풇PWJD_q Fgu"'B"Z~A7bֻvP;E.vɘaR\Tej>Dkm~։<+ӫjjkW"Kb~$}f)N;}qqdLQ< ՘N;A܅ȏ1ܭ(mY!] n^5mk]|! \ @2PA 'oco'Ɖ qk7 T+lԢZ*&pIX*FHzq}vC3GYS_T4$LlP`Y0Qa~ 4Y BBF=mf6q"`O8B=v[7&[hB[m(Ak! #S9s81.?OŐyb:z÷.͡!ZinBȐl3eǛ6lx͠Ȋ.^A£:Y'khObuNO>V1EEdXfǔ y#qWȆ`fwh"%hb.`jOggSZBVX)2)_v~ǜJ:7(5arC[3olµ*&w0+mU9+bDx x#a<54dw#'b*0Z0EtH[fnvËz"XJNՆǛ5hӬ񚍧o:t=BڽLӺ7(N~ }Z_VȽD\f"i>a 8'`ANlØ[fYa}Z^@bv"cE ǮqOBwgTt~e|$Y&ukrOH[g" h.Bm[Y]|5m'X@t O#oID@Dŏ#;*anamқH(ю؄h'<؟- i(ߏNo_k?YWN" ToO2@Z?htWaVuY8Dϧr#3Td|ԛ#h,}?:=MNE۔,opO#Uݫ_l˔$QWS!(fa67C5aۡMDυLzYB*|v{vx_tMda[hGb'^T+z6ESbD=jؽ[oO"'\% `iЁpG׵}_b;5fEΒpӍQ6e};fȮ1QzP)&=x:m (@u- h껲[0Q]-i ^Y>e]$2qW- *W9j^HPN0Ej3F]E~HFh>|p"6z{fx6>FX|FVY#[taLݖ֌5tJ7Y;w*Iifz5,0f SSxVF0rF7Ф@Md,'9gm7MԵV-85$f%5};4Ex ܸptu5k|um5ɪa; %sImZ'iyBV 4}/ o1Wd`l[&,,=!3@i<(KUD|uqN.}!;wGґ.xm:>#G3mG=$ƞV6Fw?~I21H(RTg4?u]Q Sv;Wo.[ԥ8DR&pv;}T}>~FԛSs"yIo)!{މpKUN/AOK4%zMtwݎwjRݶ۵uU).jx!S N]kyR9f >DuTh2kKb=o\]x.=Ӿ߈}ur0\{_9$~߫R@TAAoK܊dO߶>ѭo.ShEz O>dWlLWv1|9)qgssapܣ'}}$W=v*zVM^Nwj˥{ }$tk]fcRgDC럱+Wɟ|mg׳&g-k>˹KķUmjB.U $>&I| tޕVڳ\`Ou%rmIpX0!Ԃ˚U}P(Yg 9!dU`dg]y^? ŧ~*6>P;0P@]|!;~w@'/8{}kU?gmYsgmr5`AX͑|P,}Yu߹Vkǽ`h)~,[[ RRyy܄u~RQڳKm{/=κ)B .NC~p,&ş{TJX$Hmy"ky,'?V|[~ 9a|eVk=?wt?Oxu I,ֿsޘES9a5T].G8uVt'Pgm3]5߳q9s` ?[ۄp-aO[쾟~GRc Rn>=i%S|ug"?LP„kR/^2G!M w*j;OqS8޷i9LU,+aY+U!Nx`uB9kOړ gm 8K9F$HFs0v M-Mjg"Σo'LdgGf&a 14eVN2 wwhcLh܀cέoJsFpOggS\BVXpwIf68ectHTJ&u$To26$l<1F+';s.7I79t[|D=Χ/spCs;޾Ϝy ?7㞅7g'(ӑ9/Ok >}B󗧷^Lhߔuvpt$t>/p(&>J &'O.;߀|ᆟꪧ/Oo>ᨇ$ϵ<rsMX^G>F;8=klQ.ar>(;X+[g cuh.&+8}la1/'N|W= f|^ @Ny8F/٤i1[ncx @4k@hS#oU> 5@)4OyDG!M:n5s14Фͳ>|xo qLaw9,B6;?'h/xG0c,8yF97o11gNVQCt41^ct]ϒ)yKDφ?q,0s6/xŽ (8r39׼E7ylӢl8=mC #oH)G mwgf&9Q%.1m?Yvgsco~w?K+4FpYeYeYep#L#Ln#+nbqd}ΰo?gcvTz'^LFp@i/AQ='f:Lɍ\^zta$0nбS{%1ye=:^3=3Z%9}߭|p\_8s'#9]=YYӧPQ{yZ]~J Ͼ|$DTHWNjt"!̨xq&W Ms;8 GW:Bpg/j>gO2~hyΰy,[>oq12>"hp(ܬbab8?w9>˾Houǧx^ċxRX>6$Jăxd7D&x<v~'ӣ+۞ybշ tE 6 w x:>qu!НP>$:φz,N<ʀꃜqNJrָw7o<_O@='f4g5E[z >f^s98yK{NO@x:|&['D8q{Si^NǧXvy8|u OƞY_sg"ݵʡEOmw@'_Ȑgˬ8: Cpvr{~Oյj"TY AHy >G{+'|OcZCvϴsi1B3”&))uevM8-J 2xd!CҐ(t3I@ )IaPp b<2 T""*%p; }D谄>OO"sx|/^8@TvٴG*a0H 5HgvN۵u9 !)-گ^JH:m;U%T#+juUҪRuL:uRERwrPAU)T8nU)P:iFҪ-QxQR)K|r7)(#0> 2E%#|c`N3J)h7!Q7P=hh=v>}}x;z{v; ѣ@h}4hw_~*k4 'w?yC>υ3,v&Y9 ☌۱Yw7]'(4Hdcv"~| xX{ߝ;FO{a0gmG2gox`LcΉ?_is>]y?ġ0/CqCVr{\l YGO$ ]ߤx\Ryot|ؔ49mbwڴ4 iz/?S&<16)l6ϿYG_x#ϐS7(@~sA~<ϛmS5:80{m-'tmyA.)-۴mQNuy')1  9\%2D߲MS|y#wA}uYo"3 ( F'lx^_Y<;N&|2Ӗz#29}7F̈e4zS+!G}&?dodwa wt|:cI1ugFNDg$m/ф:]ovta`K|-@$SseMkYO>=|"=;C NП8&P-&v@ggBt 0Q!2bk" Tht]a"rw&]7Q G Xuo։!A *& R_X]KO6|BRR=Le}D3}ٹr}P6j}p2f_vȝBK _BDSDDUZ_+v5b|U0-3PȐTE.Y] ԌSؑ`?Vs,J/~rX_>tES/86zHOJEDJ#}-*Z*##سl=p=Jh\/E'c/ N+] "f^I^ IAÜHHCdsWI4H Ȅ8 xPvl :NN!l=CEK_::L9؂q 1 QBtl|YEK. ("lIE!y[}Kd^JM>~Wk!\nQ|PyZZȜ>Ϧ V:wwR-Ulnn'?!a2J֤>}CA֥]~lGп8B?h8R}hC}Oc> nTrj:bqD/D";vujShbET;}bhvcH NS DvvtCh-AK`pK+=̧QDOn~paC|OG` Hp HN=#gSu,||M;3`=U z{=|0=6q~R}vr:>, E~g~ OggS]BVXp}= F86%'49f̔D &ćr~2ߏ)FCD/)KI˙a47Qo<^ŒkXJ2) ZQiu[b#'o68~S᠞"^Ny|P\p`zSz:S3n Ì ;:>"z<0T >J ^O(8k&a ? r/s'@}tæYXWf /|}9L{בOL@PYFJxoiv(v'd@Jy;7E9Q:v0;=HL7@PͱZ3 UL6۴ 4?6=`5ʀHSMTF %T0& 3aO(TԐ@wwshZ& N0%HH@ÔJ!!^w"(q`ÈR >:Q 17H`Z `cȒ"$RPg>)4 x^Ǵ)>xހ>>>}Gp=Ǹ<>x x}}pt0}>ht4({vN^/;s*߆|hxZ@v  T? $G~b{R}>-33>y`y?\L[|g/#Y)N#BYg-ה{7=ᕨ<Ⱥ:8:&cnwT2݃d}eDm؉l @kGK|,g=ɶyŖX?xĨżNरuA 9.6/1nܝM3D0 Hb%݌sb#k"EfM&62s9y닛y%A룘J:?Ʋ#أJjYB"OȈ``@-B |c,@rMƲ[u># ;_H$Rnl}qr_Cr' 0ˉ!GI0 ̿ݨD~ Ʉ_>Dgٶȧqw'͈;$3$1=\F܉&3e1E}߁ψO6Ad#Z133&I@}!?6(:<}c9Qq]P"hxo-kb#3QQ1ԮIM$C(T{㬞U!~JKԤ-oX@6%LRԕiD$/@N EQ#4$Ʌ1ԠMDS~a< 2bi@L0x=9mAS$ V@`.Rf3QWߛ%FFŗXD=,u(d.ũGL R {`=hGF'đcd|XF=EI/w0VdIF2dS?L\O,V gTP8y-[BA/꿹c/Q(XĒz}[uP'Rq0fr+O2ʆ QqDe?,k E:S]/6 EKLFMve[ԑ;ﱊRb^D,UP/]?;(Yϱ "=b0D=NH)ّLZK2$d͕¨#*1% h>=gO+q] 4+|&VVXXYNLMm2V+Yx.&~Hήk&6 4yASi]}3/ƾ޳sBH<=?Ֆd2hXl~7\$}ד~t52y~5Zڻ bV|É$A#gӢ&p<hXp>AZ|ܖ@)^bԣ"=IL)rLJ0y)&% :_9w@覃-m.(X ORS>G:+@؀;;K^OHmp GC9<ts Hyx g/>r뷂MA#S?SLӧܧoDz{[Jtbᘉ0ף+9r|Jp3߿7ρ`Sai>mFU:CN}gSXSGgxH|rWO,JR]:8y=rr۸HLp)CComuR E~%"PPCȩ"v AAJ.ȣ{:i.r5k&y%c1@wqr,6WNNFy }' x~{4$-|r{{"6Z9}(J/0i@8{<ϖj߯DEӸxٸx(㱣@hѣñ0|G`?xr4Ct;>4C ;h4c@h k@G:;@K8~ `pk@ph4f|O*z%P'zλ= b"bcQ4|er" F/ܕǟ1Y&syXc|LLN' Q0O ,E: uOܗ(|c@sល|p#O!.z˓AȞ(LM~f0o1Ɯ}USyRGYΐXaNDf(/>0zBVz_)O0Bc|u9K 0Dnu6b#$4/M8Ӂ޲™@SfJ):b.8wG@1װ.?(}s9yz9^pt|ׁp>h8by\g]A`_fc̀!ㅅu !uOя3ϑ_$x~:3 g.`cط_hlf1fBRDyХB4|lq%<)'G$Ala:oap>t?:]AW^"{•5qO&h`_@3H݃y~)FxPp9\~q ] {:/:ysuaq8p=g9>waB@3?ךHfD% ʱshTbϣO\E Nڏ"G,8{ 9F2`B.S /U^ՙNBVϻGJy&~AmGIuX>kGȭ[/݅}dzI&5R>QDC,,hl*0'6/0/jQg՞QR0g>,c <~s,REnO A2qp_EI!0x_̎sEgŚ>+\p:ڻS\EUpى#q9*pC[=(ߓNyL{'65겶G[7mHy{B$Ne0 ]X8[=9fߝ~OU7z{a "8wu4j9L&FVÛhϐ'6|o#ܢ v>ONޭ~;YtN9'SBk~Ruy7/<5i9ĉ3H垧dN'w)#A {4rHbND{+H| |ODz:K!'i>R7 G o_jMkp hptY<,;z  yΎS:@_""9<.  d%K(R mu[R7gtlEm.jq3+L̠2nvEI@ @bQ$$0,B(@m I!AR($B$$Q%"H"Jx־>߀2F?t|&FDg \[u+E 8_P!_mۄ"ʎsO;#x^ '5܀lS=ċF “/_cn"2>3IA>d&g1;q;-|sf$(ٙ۠0 u:$|YDD>ch]sǝ<}[hl_^E~ eNor#w epYF$tsZFȍ? :u_ZZz\T)Q? 'y:QŴ.(6"c-|q lo1f`ۿ`#m36"`1'vϒ2LDrwQ9#'۫},ǘRכ0xB8.iS;=sb`h6뮟Z~bc Py'%rgDТNta0X"8zlGUJB"c"8 ZX{3?YPV"t&={:"*S}r+^ȿj4[H{awVBަzO!D' azX suwN>%iUoOYDy $Bi#أڈgB9s%D7-mX|+kOI}>r; lmoq)"F2J$A%M?iD*feO9WkO[Wt7-y]Ok VYʳV-EDy^bhfkBX8Φ&?}5R kgt_C01hֻcyN(IAa#ll|ȎR21yQMSRmԑ%8)cE $I$I$sZa W3R_F֒:U[K3eŪ92\pxcMaέ.2N$1x;1WǖrRyu471II: pHLZV Y, _W||A΃P3wO}^4 F=3yS" vNFuEᄛHOG֞ :=~+]|I>>>;xh~84}4p4{ ;}XhP'^$W$/XV6<#/VSN,ќLcaRJ?_A8UEWq<~z *AпEFnqš?z"W8Er90Er>g>~3] &>rEP|ï坧xr}_;Μ2+>q9sOYO ˟/y"|+LYȕfY煙dHܳ3,ϓ?2{ugڬ=m1JWISkz"؉HJ:|\McA؝"Y1OW-F$('Ԙ{1lca&In"_e*͇ybI>F9Ty$ǀ33Xg r ]z=(}Ӎ n+r`ps/ VjkFFd&D:'BN>?:D1{@OILfJ,AN&ۑ)QDy?'F]/ʘZ0M5G1"04Ny{8Q-c/ױ9>|N"}s^`|p`w7( Qzz<)) TOb Ch$nT)4tC@YO"Qr@%) }V^OO1>z+@D"pv}>F>aDz䧹<؏T&>XOMt0cg'|>|4{>)ϼ}>xn}Xǘx}ƍ>x 4h xǠxqG4>>x@}qhc4v> Oʏ!Gv5hր c 'n$,|"`rlKu-1G>ʅFy}s!f'=f'<'>P?֖|yb` >`ex1%O66ܔ#۟3'/?2~DsNQqn}9o1=LD//Z9œ#٘rk|Ϭsd|Lݒ?'o'pB|u8pІL|~9`F3k")'ln'sYq 8'Λd1%@-8;g/8`#vF?۠gdLEDž uIS|e `d=O|p1FrOx^8ekeu?1,.>:_w G L..a 3^y0B˯A>yyXc-hf>edVcn-Rg<`xV~B5Yqp| >>?gr H_9Y)-֟GeߑuBL\YyG9ťR_{4(:Fx:|KŪ4#.u>sy.p.|iF?Qf9Џ1e3יR>z݋=B+xu>> {hcuQ >yzA? ~]m;(0uyDG^8J40Xסsş lLDM+&uF5fsӶ_u]u_jtf'~OogtujڌŒ#a Z?c`?G/1m1z0qyD&~"'ǘπJ~|$>fb9w]؀>=9*}U9y Q t8 ' Ԣ%#G$xG }{aêaO׃{,吁O r|)zOjPP|$ =\ʤ>W $>yO1FdoPo |HmXQNFvR<wCCSCy73߁}_88)}*GP_O4,Q" <=tNNsB7p׾~(N4@D a]UPUغ@(a/g?2}M<< pp9ZӗyWQU jڕT+pj"'ض8f%mU]%UUUյ۱F.* 78Xnvl&p!b R*J42. QTScكGP( ]O7U؁ R0!VWpʊFUT<;($*K*UUUW JDQUBQ'ER}<>qzD1TDAJIx/*e—S2Ǚ~NzxKo]s[+{fRNw@ %##ci;`z}l c@h=}4W`߼w}x5ϼ|{}> v4O}o{8ϿpFkA;4Q{} }׾4}O@h 㱃(4h4֏qGa4}@G;KNi| 癜'u<txgQdDGX@kvqx;{x;v"rD~|✲XK3FM6wi;UXq/]o|N'lsT8m17b4Ѕ𣹿Y:(Siw=ST<& }C~HDy4p<wߍpg.GuV*T(R aibc,7Cmٳ5kJllJq d`a?knb.W&?aY}$SoO1uKOqnpó;c1%aO(9g'?YʉOH Ü\f$^05]<qV؉IM2#9(A8p~2|)]eƲpˢ쯛wjX&'fH/=loeYr{6s[!`;E×10T)q'P56g;(s T;lCx:pr8A/Zg(H>);k-Αc g Ůq8uo}3Hˆc3`#Y.fv|%Jd<n3 N9 '< !q ,Rk `yv??0gOyb'߰}৛沓s gi?f'3c1HY G?Ӿ;Y\'a3"w_-dغ,޿8pb}{{39ÃyM0̳,%b6ar÷E3\E95#=6y ^=s AOHouɎG땝*QcĦ%nK \Q7͆.,ֹv9P5+Cv};߆N/r7rlq㑒:0yA͒%Ad#1ƍ`VkWpeKFPS3NqM?ÿӟNxdgeGlc0(32}#7Osy㿾q#ӲI ,ؾTa"&$Rn)R.ɹnb|mlѓi:7<3qD9S'?BHy47(\Rs}0sR~@(!1 X6d&|DfٰغIˈyxao .: 2G14~o]BnqNLScy"fXw9&ʜg&2}zOҧ7x8Ɲ^>^󷙆n0 !la,ߴ7RMj,<.Ti?f1.0NqN?yHANsw\Iw6cBҽEsf&1yod 󱶿|n&aIm13dɍ#ΎAHPlCBo8&&\lG7_8s9Ͷ9:uq0 0fqyX{ύp#Gaq5B3{>x47-_N9syމOc&T(:!٥N{Pl8gT񤸒%FG;}#ˌyç7>gsYB=[>u?;YƵ#aO·ن\k/>aZ~>2睄~Zl;oB>ᐍ)WhB旦aa#Q"Wio'}C_s}IԝNz<VZ~@ 8I5Bɇi?R5!cDZʎI.2A9ZjfsDO@ 93ki馫ʇ|ՉQ3-X0k>&Iѩ` ADTQ1c" E,حhZ[[' hT$D.P-$e&E|=^)W$VHt N3K> z$$Ԕ(5I$‰Uޙbc)˫"BJA6!jaD%NVOi I/IN$ɅP K`P(JPuHT+ MNS,zJDQc QCʾB %/@cF2GRJ>[x*ͨt+?FeuZʮA lGTG|EnxVAX/$M[}RcVBH<B//xV/W,Wc9̪q\}'VЭ$ޤ |H!dU$+ tO/|@.~DQIBb_BJ(,W tJ@'bMYSc(=iv+bIDĈF1#ZEޒ)Ya[ :=b {]O vxlyo)3QI<yaG<:y<WCݿ0gG{m9ڽ3hN%zLHk=[bKVGiS (;<7|OjYlVx=A'r0m3 >iCZ:o|-|+UNO0]s|ҟLhdʕ*8;r!9 t~P|{>o@tI:lh CHp2&(pߑzr!;y>Cm_gȭ13OG(ti{>!Ii!Rm[IODu<{ãY&O4<0'G,"42wp@JQH%*$.>,Ҋ$6p%WucMmw;pJmalح`݀8wwwm0`,U0U`M-LrT5Jb80[x@NNwN](pH[0ܪVT|\~uYA)U}ܓ%Se# CR _D2<3 X})YuV/\EdJEҔBC(#*A}֏i  S}c; `}@>}=Əq(=WkG>h{<}c`Ǹ8 >}}4}㱭}xX>4}v4`SF;z44}P`v '}_|2?G+op4h>v4`h54 7ה$C:dK1@m⃖0A$ AuG 1xhL31;(t-" a^LO `BJr9{#!s9: %sy>ML&gse|c!eug#\Tu7r؍EqiQ뀶x᱌mmDFso14_NөK YٍZ3MuY1pYӯ|#˝0Q9Ƒ)~Gݙ~P.? ߸7Dž၍ڎDLO0} eLj;5GvA#@x/fyG5rKڋF< AP1Άx8 Ϝ6.\3K9sDeOu!|1s\>Lds 㭷E\Gw57=0EA&gh!.=B#~11ka@syG2ui>]N(:^5%UdYP]{ <=1 [΢69':ѾtvaۮsTxO9x6u1|y()%$u}#I(N7>Oc<>3_9@:GDOB_em.]MDj y\/;1mJ:\/G} 8WB9 Nk _f<z.d>,-.$&#Äiugp" J7>səbO7DwVE#su&d$/ 0w(eуuԆ?l̩'utcߵ 1",]0`^NVo'Ab{"P)@ mGhubr4 #8:! mg'ov^qo!\y8!@)jA8:" 1@ ~~ nbV-? N={r@KYG8#Դ891؝ h#I ?SZa#mc yy ~nl H4"7nN:Hx>alsE B'&qtu+>o,2fmZO\9gȑg,'3,gDdnƨQ<./8ƒL z?FὌQ2"U܈I(u>]pǟ|6wcu=M"XLOEQ1r=CܥbtvIU58"4F MAird!:=䱄>%kS'^zIZNJI$<=TbUu^ڔ^otg䞦J_|gO!$V%Zpr&AmI>YI\=3fi؇ըJʕ7A zrܗEe!XEVUi*F'/)ѵ{>PX'z %ȜfD,#zI& FQMXD]Ҩ|%@rGdFeVsXzT2+_$k@537QfDwÝkɑ:ﻥO 4% m<aQNo1DGI.QC|Dh?_r^8( 1T`B+RzTTg(pDS'E؂ZCBJ Oca6xPpwa!ǡygASIa"%Eu).lc(*H&?z A*AzQ,b ]I4b&ֲVk35*`lƛY@Ӛ1YHs4Z?bG|yWR%$H}Y yG$NPWd\}6Iueҁ*OggSdBVX2G%f+ ͺ]x/F A$" HpWSubt)g<Ŋ䚇(팲<4`aJJ՝syP[xH^޾z2qdjego'kZ܄򞧒NJL[Atrrl}nL{c}-<]71 L|n"H?!HyeޝXpxF "VyGp15o.;6qղqdIA7Ux 'ԏ٢oq{N.>p|]y$ h]!qNr{&Witj~0*|^^APV;4^e}OG-o5 Ïo==_*3"rO!B,/7t&C<09>E=_m-2s)O-Vgn`@hUB yp@pnJ/f0 AB !'ԁQ6eL,wuY6 15c`U`NfqcBM`qR6L83ᘶZve 籶I16t.x1= xhv4c>wO}>} y㱠>`_x4{}}>@z Bh>q_qƀ {h ;|>}xр@ ~xF;. ;,?ϧn_[<}h4vѣ@c@>4}v$N_4fMp'/x͞ >e@j&uRgf;0x!\ 6h,m10Og!,,zrٜb &vAxs>' 9 QYY\:#|u#e ŝk+ɾ'6&<,NjE 1~`60gm&qݏFẠĞ<9v%K%Fs>;GxN..DB;ύ٘ɍy__įxNт\|-qGnf< ɑ!?B2?%y ,O9Σz=Av- Mh_7!>*oɆx3dgh#XBF< g>FO'|uΤ:Yڎǃi6_D9_Nl_ D\g"I_1N]܇󏈃s1J>oY#y  jj/*/ϗ%"9OEWPln|GV;Y~0bc$FvJ ͷb6/S1_'?Ns9}gV{' @(htgY"k3dOkZbљ-fE /,d\j܇%~IiўQ1(qm=Ob%~OFۨ#olBdLV)d[f sBͣ37DT^ I3dE Wf$y,3m_|vRs( mq[L14AF]gSy?hB9e|w[=5/^c^| 4>sm"a<߶bf=YxLjOj &#c:7.J^-|㣿nFYz셾ݨoIf53գO{͐ vcijB6~}@0h̼Z>7 y\SCj&t{;oQgt!Cs c < cD7;9xM&;Io|9o+y_ۖSn(V?q@V3 niX2!g'{<1'_jܭ<#G^cϏbons~!DݤIǿ~ 8QS>-};O oq{88/#+'08Omm1+go~dDђ@?!|(e9*<:&b+dsLYt|ȎYLkJ`V|iKĤzOCRJ}@f'u]}(׭!M=vɜ=~:8W}lC YDT+3TΒ 3:MBzpD8cS20+K*fʬʶR8X\_(9ɥVM` r WpUn.U9!v;M*6(Et¨TpDorOg?R݃PMZ/^vV=BB^ 8"&An/:OkE(P |c'޴?2,N˯U&_ 4dzƢ)ZP=+RSih)hOQ+7ٸ̥iZKiia8j-ɡQW*CT"5pL "҈)TR=hBܔ҂V(=cB VZ!+%҄J8@$mA!"zjMBW.[l0e$>p$=ʚIeQc%\ʺM zIO^CV<}?%9hSawj`Gdja9IJ1 r0yezuLꊵEp1DxF OS1Tu1uc9!9pdcSP&Zm1qY~QdfN~+[夬l1?m[v$ǿ#tÙ\ׇYVկE[}xz-ܟgPGI3WՍjB:c찰'Dj;&\Q ,^mKO &%dBI%dI$&_ZǓ4ZQ)3H1?r0xVrq<<TH^`ZmO''DWO7lc%=y'sxOCLAxka]OH#y}R51ۅp^'Gz:xuᐾ;6:S1N9R<]gDc po1թINS3_^yy^yyt::yϣג#/`y*4$Rzk>Jy:XX8Vi]|NNNSdD|N}?<>G|Xpބ28i7bu}$h}NQvOggSBVXZo,^mbIL*s"KȩˣOBYf'Ю(Nx'(> k D8jo }_Æ[Bp3>,B&G{dRNό:̇x r7 ORU2>p8< *̓٥5ߣȧf`VENggjppb#OiL=G'@@=p!4|y}Z G4>eg4PC8L`';ĴЅvA>=|i ȁ~]V֭[[5**nU[ZmUw*Tnmx nn{@`wڴi7x)[C4o>vXp}@{}Ɔx|xöhGaxc]! P{khPhx&vZ4@;k{jWTh{} |~oG'lܜ>g:|/rO `>hߠ$ _[=тNf ~g67 3;1Rq‘}9#ig&oqs'OB.;fc)sA˭}sqZnyO AGA=n4Cǟ,Ƈv~i^ cM?cyT?n/wl5g~ϟt[#{ϜmسSS'8؟;>zN1{cq-6%ljha`7'Ι|1?6o͉铌kWT|Lb5+YܞGn#& rpA$θp\6ls2՜cKSHc1;߬8$S\(32n pDiܧDNj}|Lm2TNr;|+g1PkڊaqإM(/v1N<[Uo8b/pFXnTSW30vZ!lff"1̶ CrDww'ixLh~մ5K f,@9e1M(Ż'_[z˘&δ9 3wOV""3iȞS:9:mE]d7l| u֥ZĿ,ɿ ԯ0߱:*RL{qf4\>ϟyq#.C9I=RdUۿ5tՅ֒^k#/fJ6>G{ bm\0ߋE糟pg8p>1CIRpi/E.p)?9h`w1ܑF@s{S L6e? ,+TYYm ˛{usf*06N-v3}ˣ 02FyY4y)AH3`-$ngm7F,˰Z [sSؿ#sR]y9ύ~!>g[5M[$3̸0(ܠ֧5jZ[+c.+kc2Xrs1sfrnsL - pF2@3&6<:iue>[ T2~1f}o_zwX_s,GZOeNs}e,w:o*7^>LI;O4>u}Zc4(asfmጺk'cŔ(ˍlXܶ31wDxYp3DN1>w4wF7=oALLXf2[g~5%}RK, 6n |e+m\XY;Q-WWzf/5O7,C+}\$jXbQV6'Q+$"&^RײG5aEkIg6S5()͡.ae$("TUV)Ul$ [Ojy=!I)-I[/oD%J16?XA(%" BBD1M\ZYc1X!| da>7֛`EWA+J0JNE(beLB.ZsD|6XY1~ȍcVjs! 2%ZID%Bɋ%1|^p)Ax!"d+"Ȝ?,F[|9/hADJ$y($pJBeU~j%QzR:~w~F?cH\yOjFT] jܫ"tD~d\7"ZVQ1ʱ%/O!ސ %s5t@e!h ȤL`t!'_R2q18GUc:ORZrrT{)%d"WAODՐ}bB ؃$T jNgwJJHPM̼ H/  _i*4@HLAHB$@Q0$ 2/)^dB| ($.$%5 @Z@UTt2,1ZI|BJS( T(0TT.tZ_p" !"^bsZP B{Ζ=QKK_ a%tIxY(LuzDvj*qc<"3ʒ1&|\OPe%o$?IBA6}6,~3E艈 cUܭGl &G" hJ'Rd c[Ҙ! $,A$D՟q'6}JB:HVY#(HoU!$'ŕ%',>Y1jE2,U~ZQV~7-dRzD"!,b?~LVkJfs rOggSeBVX(=W0WXJ̓%bF%-VEh 6dX*yIhƎ%XF@ |l{ԎR[<@kfwn>{ "ȫ>ׅon9!*$JGI=[eŚl=ƒՁ!k",I$մBHy4Dzz H k=%dE IЂG$>6IHV^y&l~I$JJ%t$I "Oi_" OXzVXIBT[H*IBB] h$ R! $j%OkOҺ>MY[JW#+iXZĚN<_GABgϙw ZS=N<׬7JP<=Y*961-ə6uI[Vh7G(}dOJʉL-B\%1zl]=N]ȴzݵ_6'؈V1-Wʽ/%L/۠e8ZstZs ﵕְ>D$idhC.@N9o8@pQrtzp[G'$W;̄x*T{r>$yHnDuN<S578/7:s:!>)?x랸yyyy7cz_6>'_"rPH駂t_Z|RS*,a@qy"!zM>x* q N@i0 =2>ODןOEp<퇰 3£8)Ug j觩ÐY<=r80 ESug~7 ϋ O^^ m#Pׅi‚q '"pxzDی]iPHcpPŒ^v3e=P-Կ{ ҟw0uD7 1BAxQ4Q498PۏG)e{@638(df<4 8}ȞE'+Qk/gGGsN)G*R";s(puVy ?K: a<}O`5-}G}&$>c|:'GXx@tޑ0Nb!9 AO gg :<1 닇&<S8pRȟ 0]R :#{)Sfvy;qP1<H|90a(rD" `y9]{<=3%4DO'"{e'8p*wqw#jUNAUUdQEDmQ*L$XU(mks[ *]gI۪SSTJY(KsWVΨ2*TUKj;faD*UUXۅe*Ul^36ݛo-BnySX*"XaDQ$**RN{-IH@ XUPTJJˎ"vP*DTy%@)JUU"P)*Pxh?3()k TwВA!ϮQƞ\y )wGOۯsJq\(z&\tuʒQH$"SZH"z7V KJEåiN'J]KGVK@$LW]Ԟ_H:n>@x3;4P>q>`߻b ǰ;Mh Atƀ5}BT@(v>ր j cG{A@j4 Ak MP`h@Ѡ4ePP ;Gc((P@x h@4Ѡ @p;@Ah;hB Ҍ4v;; h( !'+qVQ$IIfp8¤#ɮb|S͙9|```#V<ʶv8IKg͉ `=xǻGxSGy?Tp߃*s,w'kI<ȜN &$fH!693y}{z#7!H=B7Z<>G$N}Y3T/0oqd9oRq}Mw ۧ:}`w|l<ӸA~'߃r.S|˕>ɽϗ886MMm1 gl9o#MoȜpbƎci6<;:sGZ;Н 3b k7VQc Wq{lE!y[igHhĭj3%4`͛ʑ?ݟ9a2L[ks%l,CX!ʈPB3m0dq.03s \*ѕ81wu2<9.ϰ9'>6Xډd_bf1g2T}?=O^.4ő3?7z}i gޑp8fqaX4mwA@ Q) iOq'ŕ9CSw>z$G`7C 2SUnc_HIJb";8$]Z3Lq*L3\c{poqNgh`%FH9ȿ# 9fYI8E㉞1pD; >OdMw\ZB,_#HٚȺm͛%iŬe|U1C6بfEdسlb.eo{HF.0M9Hs}AzMs֏v!埩4oه>I)-i2Tc2g}"᫿L>8q7vxwO=TFFZahͬZ66˯Fv{~ÕYp;F–?k>%ÃaA,$sHqiR.ǤcIoW9b|KK;w.D) Q}1;/5> zcjgmR/T]Ĩ+snŸ<|nx7ӛ.nѻ!{#؏/%b͚Ƙ!Ǟ =p~<)T7 lG%OȘEVk>>q؝GYd͙KyyO .pop$Q�uy 93݂ss^S,˅B`7f7)Pˍ:0C8 ?) S $Ծz祜Z'Qn‚+Gy߅s…$o4|4i1,;0k~|/zT\m!6F)O4șπs{\9֏c/ᕱY> pLZP|oizs|Ly /`G|i{ϧ~ċ_~m58ov]&q9ed>y/BTϝ4P t-NJ29Pa:-p vv6McΧ]G#!e <_K|yyڽ=×1iiw/}fHfVyB~' )N9Hwv#}n/Ljn"T8sx|Yt|t w8f8y dm*c*.\W"467KYQ0^ei9ɢf%6D+X, $PLn,uA(0q820(ؒb=sBYԟ/g%gOlQYA$l2,( (]>[G'_x4薡> *WMWUerN(IIP6cQֹGM_S`z+<% 1tJ/>U¡=@?6 *';.Bڸ"LQ Q|V࿭`M&E9w ޡ]nkm$8}e mr3 YQ W {a$I> :1jŔXm9<6{6$ݹz]uKB?+&`j]T[+4]6ZQAF?*CONS(Ց v\D< f% J +bpTR+'1̣X${Y<A QW(X'DإK4z 2f>{}(B_O=ciX/Z~^dVL†yL NvgDlaTq\O|Hj $Ț4JXyGkNOA2Cr }_cDI^l\A@rw[& !kr#`)PUd\_a #Y ++2S01X˽퇐L!-;B$2D-suNT9LU_|mlfTznL'#:0WQre_yT/ZԺkU\}.éLx{;v2_dF1m5u M1"ng/P}WKם<}g)Zq=w\ZS`F.°xwEatw{a[#nv2]+ܨ)8d1 QE+\WGTԀ@È}5`'< '&VR1!≭>( `P9s) z~Ӳzͧ*S}+Ǘ<:ȶgNr'Ǟ { @8B3޺!XfC ݩy)>tyx <7#@m{{LzSoGwpOdn0`1A8?<Lwe9,:3!CD1~WD \ߣΕߓA|M =| ˕=>>ͩ&+~oe O< H/n5;) '8<=OSt;>_Pw|3ONɏy}a45os^O1<$>NȞk)Dq%Ȝt(t0>'˓h<"`xD$Ig*IOKme*;Ym DuYz]Hۀ{s6n{7vIJfm08n٥l3p\Fw A@$JvΑrk9|6lņ`(L-6pjg,pD/HXUR\MiI]%XB&({{/4SHS>`0yH%)/:>ԗ 2~ԐMu>Z$0>ƃ@;x` c{@}@G}=o>}㿸xozp㱠 Ob> xp>4}p4cߝ@;4v>`4v=4x5 =h{4h qN hx sˇY8IMzy7OaD~~?;5}4}@ 1$3y? F56,?.ֆȢ7'7OggSgBVX سKNvhPԍ %2ŝmGAAת\]~NLlno;x$4He'؅9>.(Ԇ^('ӥ>b2e0EżK2{b9ٜcr-^'WjׇAEvvoe:]Ai&] OtT6;BnKco#>IJ&VNmΙ4s8`a#$/xO%;o]'v!3ZL~b`>sOƇGn|u*_^/ÉO6ZC+?G=<\z\Or9+XVvWvS0vCOL{ƿ>dE \y<dG_&h?>=*#qs<Ӿc۵,1H<͇]:F8SV"Yd0=1ӡA[@9%^!|uiKp^0/i~i[N߆pܛ SM:4),=pr*r KD淼LXP7W^a;F X M&"F qM6BЭu覲0gp}MR3tBU+i>Mj6_:=忮;]Il'rTߖl`i; ĥ:&v%fRyD}ߖea\p%lGR jt/l YjB~ij-eE-WT'2nЖ yΌݷB4~,pV.4/Oxr.%Ap"ςr軁%=AVԒAt G]( lΓԤ­fkcb(AG%t1Pz$X%gW9ěW pn[tԂ4"BdII2K4l̏$XyEH>x,N+xz3M&سI"f&0`Ey$ZHdZcJG[(X[+̛k(dcb&+Nha 2#a U]DOwsh &?GN~\kY7\o=dZh~OE<:yzWP>!߯ y0<|Hx[|O_#>xϴ~O :> )ח =ti@ e>gH.!짧_o'Š3 vpsȜ;4첍3ɇλOO>%~BQ"LtzQ=_'W}Jq({0uHyy=sʗ؞E5=pnMy,<17Oç|DՊ;?o{VWp1'}NQτS㜴: y{! ݼg$Tj&~B'NE;hƏ}G>{}xhhhhѠ;kxcGc;};4`v@r{h@6{@v@hhѣpy4Fq4 @}h<{'4h*@k k@@h~ //)2Ψc?G<x&$AԖ"2uG:# BÏ@#bW 0t/xjd_AO#&':*Ua8qcNXc?FS'^(10rq,z# 93ŽIi(E[[Q<(0Q>cЀrnJ#/9iq!"D<=ww?I2 (^#qXP)L|hOܧS8.a $?OggShBVX!0`GyOX1 Y3L?Z2j੦4]%I}1y*SSF'9 qf(] qbfX~&,yz \uQ>YoS<Ne^iЧ7+J上f<{w,8TqIn=v=s~8#=f\DWj^j"A#=O߁u%VJx~SzGCc]g`~l`?{YAtڍC$mf͝­.ys ܔoVwwB`ODt҃~jZQF+ l-Vp#r\M_Ԋ1>J$z/Vj$ ʵc>ɵ#ktzLV 3gZra Y[!iIL+}g)Q"A[HHDnÉ\t#~K-QRXO3$Q䶢sR ,fWdKNqN!Cxy" @+|eg׏nQh*)3 IE B9$"ޒ>~E l%P=|z\[vnr|J_ 2`)H"SH"y)b`VGΧ-k@`VeI5D4=cЈ!ig<%p6k.PFH/74{9_0al*)X"H4JM鸊!/X?X|~|r1ryXz_vzf>X]X\>,OKJ)l? {Jŝߵb՟d"cj`TOrE_}64Zb7<[Y7 0BDG)1=y7F3h{%r\ǢI' p%qFD(b·g#-8)pa{Q?fOي ~sKW 4 s2Oaq:4,V'z;V0@ P"a}@g94@@'#iZBů͌>'g룕0# /Lz^th{(,A!JSy8ONEhYK ڛ锵aY!,z= :>(v B P}p^N5xƴD$R HHN LyJ$iHH a-c9(a@=>}( cx4}>4h }G4}>q=ƀƍ4h{hh>4h;xqhƄ;4}4h {hꆍϼ4{ @ }§y~4hh h;@ h(\}`DQGQz(!w# tg$xy5+3"~qhBs>O<q 4VEH#8W πOng~r t[nÄL3_vaz3p3NAJ?0}&eIXEab|f] //ϟ;#!18Jko>=d>N㢬@OJ9\ٿqp Dt[/nJ*A (a>&0!GY(c.z90"U^ p)ưfB g3Xr .rĎww?=+ϛ1Mvp]w,8\_'G.~w rx5_ïgJ$,A!oI]G?yX0?y=ac4aG~ ~_`?lrt?OggSiBVX"hf\=z{A,Ga @=΁\_5 $EŒ@7 wXf!x_3!lYY(1`0OcU硸.V1TमFAγ(Y~D7cnga _k"T-:-#n{Vo[(/1< ZYmݬ/ LmW N~! '/Q5"p2 Ye7\Rg>e Qp |@ :lJUKl4p_|,RTU Jf!ꎤz(lnSc޿(*=|錏1V:˖'*M;]-XF/ymu,m\-X">Hr7߼t fX~EtH-hv7ԃ2G<+ FqI[ XSiM jcYMMY ڦ; z K~_?xQ3=qv#ɵs˵{L=2 iR 2HUljp[;m,Չ =D@쉣2 =`I *43$8ORV8_ ,Mb9U3d}~Y|Ubϙ 'ؚ?1Ӿח1L@.Akt K;RU~ g#h^GIIu4!$p%Z^1`(I=y{\TIMO>cҟb#1>oo'[r@(L</q)@=htwa$-CSHCthiR]bT4up#Cw-?K+:"^|'߄`wBq<`Ӏ;P018U%E HnOG==q<}/>&OHo<73BԴo8J){F szI L pqO#W|K{B@@ @ =9 =$`@6#9I@'{ 5Բ5oOH8,0b7`om Z)^Xb=` =xOFaz5 j%ݘa@ CĂP;`q"?B 'xD|*Hˣo֏[7$L9 JN]Ao?$e<:LΑRBqc~0@ &xhxƀQ@h>xǸh}v>x}ᝎKXzn>}րŚ>xp h3h> >}`=@h@/c@ p? 5(`5 hր;h;h (Ҍ+|7&M*~^?,`e0!g*'c18<u܎xA) :~Gv >HkM,d1yо+`;t_{/4:_ڼǟ!ІamtGȈ7C,'<ʖ (RX3oِ1)P>ã35_\E_/|sǞ/s=3o}[?7o?g9fvבּ(!Іp|b yg(@onquaϗ;<+R enNm(?,r2u_2GZkP݊9.͖X[(FF<-7-| L1Хy xn}K(o,ArY<|7)qi6 :}ۜ.`'t!|~LSsGIO_:0ORxRe6shSYO3dC_Fk?a?~1G>j9|{~D2 7>ȟ??#2#lqt['' w$S?ࣘ<+~\B[e/ oՃDnei 4( —gG -!}g]L3Ȉ@&ȱsꚤɏ;3ד ׿rmB:Ǔr3ȥeQ g}H&Yc}"6r1:1:&?&-nzXłB:O]u ;ܦlΎӅ׊ς@b%z$1$yB>R"ϭD_`/MP@8eѵa#0 gD, B\ݑ9zB=# Fxea3r{^]K'3>1`Q''$8PpJ~7Ή$_6ag= OWW2TyV"'!eeAPD%6X1KW0!$Վs$Uc-t"I9)8[=lϰ+qnםwrfZ(SQda~ɇl6Sdn}Z-?)GP^P6R^WE4QV=c2ZM!r(VZ+5F/&*10m'`&m>1%2bI8GcƟVDEF%Y%k Np2Ra20|&}V/N T଼$QPQF: f20t{cFH:+(Oy r@MNOR^}&St2#[Pfu#PYDS0=F?/>gP%=YlX7y%-fOad# 쨛>A E@bR$X&b7FR/3<|>o`{=;+9G|a}iL8kɢ>0-D ~(pn8(rQy wD;pzi':.U6f=/'F&=JWš⭜4rygtM?nIi%:"Rtzυg:y0t$>X S|q'~BDh8>s?DwH:}OPߍRY=-OaON@ |!nӕzR UD8y neOtHU/ |4*s:rrm:ӵ>VOe+;iBbq@,ԏS=`{0y$':pΨD$vR4Fɇ,qCJ~=>Bs tHy4 p2 +|?R+&$ӚA7NWJo"!*UUVTWUnn3PVV\Y-T8Ūps7EcnfJ+լ=cU]̞ե3MupB)9  > TUU0"drB'pD^Y$"P1"{N#  (!A$xǸG}}}kG?3)\փA/ILLmJiS&0~dsoXɴLf9]`K+3,8nǍV ͺμ(R(̷ݕLZ#Ĉ]ssE.?[~y|n?&qRLr1I#Z0J ߘueo"}tBl /g6w!N?/g^BN||.#ۮDlsPlmo:,q~Lт'+-NX@S1׃{ M͗B̩/33<[!{uEeB mG}DxBrv~'JrF2cFG;^GGv7 )Mkdi+AW1BDFqڧJ.2 cGzc ;ϴs6x&VfQ9p=_+3Q,Q' 2Nil$?ZH̠͡ELVLI}$UbcmX7 ﮥJ[#҂'b*ahg4cY\ն£b+.~X3to/+6tYla٘bfWL}gђ{ybW؞9(M'΍Knp]'k5 u'F :΃@ωgڏog0BE0:N=NO"~=A٧;)N*:4xX;0m9~&tut#rSrx|X>ʇ X|Vr0+(`B+9 'F郏`cvNb{NgW?'C;)_#>,P8\G8 }o ?N!tQsw=s2Qu0K>dD2PXBTbL>}TF1yP/D).dI]Y>4{p>xm{ =ƀ:Gt v=}: Nc@vX>><ǘƣ>x}ƍ4v4v>:!Ǹ>cv>x} =}=4{{ h> }h>;x;v;=v4)FGBy? /7\Z;Z4`F<+0|9%.L̍ǃn]iLbA֑([?"PPӯ1ΨjK,Pz|u|Ql;}^2G5rՉ?ٗ @\o۞hVR)PuS?> 1*NYt/?$:Gg}~ݤydD~GR-#r مjhceBD/n~P!xRG^W@pzߞQiH[[r1lsDZ(D#=!b.uĭ8I?q_vezYQِ-p33$&"#&fgIilb(j,eleI`1@iN@Ԧ/"/9؋Y͓[223oF'iY?dLm]<./1YېEpؘ]m|R?<m˜Eaẹ"$\]+?#íbo 47Sz, jM3,'H]N}õh^.۝OggSlBVX$ا5_׼h/F-~x${N|pC9Y7l?:#FY/I t_Y=7 wƄ#uDzd1gmRH \w|归gu:s /q{`@}CcoI_$}[Ѝc$o߱K ?,ze B܍Rs[Ghtpj`l[`oX3#<8f{cr)A?anңpG6']qfN G0, ed9̲0rw:喩@RQ3G,gMǏ0pz {`Y-:mۘLNΜ|D/iM]>%~eBE?\Q Ts! t\1h(9cxo!KASC^ oEe<1J0- rBT`hQG}ۢTbo(yc>c97s\;' ՑA~c琿G:6!}y#k? J x~>{yup B.ușQǃ6m@xR ͟p[Q?X{Z"TfG XJ1¢E|VtF $X2%tceȜ&)NfdLrO7=N@NeFQkȮshVDE}V Kq7ǀ"N^4,cV'+YLYXl\ӞE+U%rJ2$I-E\_/z*qE5hW--z\c` N-j?(_c=O_[Z";rk%ˢ>C˟{i]S&)i['n= ?gQG.Ȏj3B?Jƣ5";#Dt~8IXJYJy幸0ċzM)c$ em;"Ҏ63U lQlm|R@d_WMԊG;,#'XWa&}}$SQ A%eqae-,7R%'%7digK"cI1b2RlbO|$R*fOdɝ10rWRAi&T3S\ϩ>%%I.a32OZ(]SX#v.NlO!E(WTp5G~o\')uM0EN dqbE:L,tΜ8MڜT՜1jn'7RtSvwy'7qqx' <<$8DLL977! 30~D3G:`'J|E6>0 Óױ?y؁g !!~$O"rO'c>C}`}5~'GٗN1 C>BsDi7ghy>gCOy/` p}\z}(*Q\x<;:N DT"{Peɞ'%5f^vgpz(yI=M9myH^`9v7 9{>Â| TyCC; ;@{8;>SA8$bo?Bs2.n gy8*E&`9!a@ю&?S8O "*P=^@ k )z3oz@6.0ٴf"PCH =m@c I BMi6l`YͶǞ,pp$"^x:w_;ﻻa qQ$"0D@(LGMt$H’GL )^@bB> ą%A~ dQ D> (v`{@ ;+t (Ǹc@h}>v>v>?Ѡ>qP{`4p}}`x=:x`;4h >F 4y+@4{4}{;0w@4w @k@[,{??ҏD=4h` v ?dzZWdcXf3 ~zAGƈ[# ArcQ :Dn7i$y_>o2MOhD2.lprb8#8qR>83skDv/F~fϙSO] swOq lDXČ-`65M+u4|>34yS3 N#%iy] ~*؈q@g8-ߡ^@`q%"@g?3? TALs 'F(;%0;=bBz~!4atL-7So61"$ Fʗ⢲9 '`q EÝKl80semP˯3!SE&?Fj7AaR4IYi$&b"cz \2esBGw2P>s)h % U2uDX6<}3-*#a )N D[]%nK#Rp*"AC}ԍZ,RV8Q'p,;_59^#cǿ68ujr~i|:Q#M-ON'r%{8p{8v'Ƞbd|ttavxvJ6hx!Pʃ>'Arw#OѝN'N {Oo'sCx<C1Tz0٧Dkϟ\DFrM}3 Aji|Mp<# |! {$|_GКҘzu}Q,Wȿ G ~>NN\9978pQ%”O' ;^y"yD9kO<Q'$ pv$) |Z҇G`< 9 K>Wi{8(=_aE0}<=@*'4"3÷{ E7ɀxE 0 1n޽PX @u4!E(nZr +lz&l h6=@zdcXÒP ]`b`D$ k>< D<3<" x>I5…3 A1˄qH5AD'#\q5Mr$aCT/z$xMQ@HE2q$H}?}c=<ǸgcA=}˟x}?x{ӱ}ci{}x?xѠ@h} U h){Ɗ=v=;=Ǝƀ`hh zƏg~9%4p5 h@r{g1[ARaI#| q$!LqF x~L^}p{8Me?ĠqpQ ]⎢<0 |T X!AϢҌ9:QL. h9E\s m9/pEq|TJGDu?Ef |H'MzLPQY@qGnjgq!~gMR3`AH uQ=wyqt[Ljj(w?(+Hr~:4 %J}|M(ΝOޙ $璁pAVӡyb(ͿAba݁ ]!.{"\ Rt"+ _̟A !00|qz0Dhs=|s?w sfR ,?=  s9!vG~Z: Ҵxi'9YLcr)\Xዂi2\s@@ B$x'z?b "i"C 3~XGh!|$<ya?Tqۍ@[pDN^PXxN'd숽 /B8:& \g$Yア5ă76'su@.._y/C߈:"䈄x0XCCLm#[3~u1CהS??pc1ߣ^<8േ?'s8_}m|B\]z%>!nn_ >%,GT.F;+ o7+gapJy->\ :տmrB7ieG巵pؔ%++G0+`ףf+$GiȦtF*_dO">|c3lɉʎ0R%RLCs3PGuε_ZS޵dWsE=6ȇlO'nE7OggSoBVX&)6p:O.9"G<1LjVp8BM+vp]8ŚDM^Yͩ.s]8cݐUМUڰŜ<\wS8ng!gbô})98=Fo/o,)8DNDB#gч/6Ҹ:9{4W:S׳; X#>o patZ<'aB>×\贤(8>/}k5q3X~O1Ӄ w|4ɠz>~QyI@Ls&dt M 0n?a=Nʰ0%@c;a m)?ϗP\* NRp?X掄J*0ܾJ.7 DPd^TnR+HPE);@ݸhw>;xst;; 44|>q>>ϼhxh;0ޅ4 xǸ =Ǹ}c}>{}>@v}x쾃}ƏaǸ;@{Ǹh5={ }ǸѠh{; (hOkW, OG@4}Z@5h4( G̞<`k}6.nK,s< ] ylZu+ןH%,gИb՞DGFGzA}VΨq #A11P~t'La/DD3 `,yV@Ȥ_$/i)N3?sYk)>7@-̢`1? /?ܿSP(FF]fK<+2]LOgZD]dj62\A+? 0 Yy丿G0!Nzg&6lO6sgU<.@RGSo~ʖ~S:;A etc/ CA~H.LCbDE_H#fxL3Կ1FVAY|4~{xx"t?_Eln>'Oߴ=hB5 //.lusPQs?qohg|BgmɼF! E?6fOu0dxF3UW#7fyHǼxuˀ>:}ia"^^ 2Bs^BƺWAZA|`''!s/"̌19@_s78j9/G~@/? |3Ӕ;ό33'O8 Hi񧌲lNS})12QV&&FJb׫:1^1ɦ? >6OovOϖYߘ c)1iDٯ | DqN"pcx3B؍kט 1'WaYun.(pTeIُ O x.y#/_Ԯs*1eA_?]F9#=|RnB̕* !O;ldQr#G|(fqdj.t͏/`G9$+dFduބXeFys'BD9J )/.3u(pK3U¢=vRO kzrz >Q6+|<KJORF/Y:2=ԛyĄ̨%sq`&('4r PLi(nŧ B Ljmĵ @/ŌT'r+.9KbeLehQ1Za-u!t/&\_Uovz6d#j"ka/^Y 0$a#hk"(/qPn].4="ud1jA}v[L;K`Y,atc 2ȍ= c:%;=R& K9ꅟDu^c\>iE-Nջb \`A̹|2{`I~ދHR/FUdmQJo_?%9n~ً M$#[U tY_Qm"f`WK Z J?^ mnemqz{VJ`"pd%X]yVJ~$MugYimJgy8`IX[\<7lmVl}_Xo%ލ=$*A]%icBtCMI?e =!K+Lvǀ &ٓzr1s+YQ Z<zW6mFX Ϳ LtHˢn߾]eQV QYpguxg |Yg'X mu34$8ĐNO/I!GƑ92՜,⴩TSVp[^z/q מyzyy擘^jT7"[W^ӓa&á9SLy: d ٳ ~Ot=N( CJ 'gv>OkSV~]>^}!x8 Q>@7y:+=CT!ʧÓʡоdf/~̀pHަOOƝ>px| R=D)Nyi}؅- x\rP9 "v{o/Ay~.+ A8Mjr#+١ "#F'!'C'j'S<ϫP{H$:0C9/uboIα?Jr y78P*nČ% "R*UPTxb HBsǺ6 ;(5jwspITݽ'Nuqó W-9\8hHNQɻ\wpჩH3T5Kc$+TS چ{ l a9 v` @wT2}6 0HOggSpBVX'ga[>mf|ف[dcA+øq B=ؿ% Bq~1{11D7v,QѣA%"@cȡY`B!!WD)򨟡CّKWPOxƀi4{=G>@;q7Apaƀv7O}>]>Əqv><=`{>0q@ =ߧ<ǘpL|{k 5=Əqh٣i4hh Gtѣf4l#Fl Dxv>}g${> `1h `ݠ4@ K==f%~ 0nD>b5s֙|~['r'<moȒ3!#Zw2&:XS~Y"oO^bt/_uk w,jp6`JϏo ܺ2#ʟh~tF"y'#eĮsv7A6r,ͺ_?:%N?htO 2ͧ9~Ƈm<#([N΀'eЄdY-gy'ٍE0PPB?=.:B/-l ?#oiҎ(#N ~RnB1" nw{cO2z3xR^Ko m?dT"\f<Ŀ/^GkX?-X1v|N<|Ju>BGp2]u [Lߛ8˭a-ͺ#j8??_3sz񉘖ra1h.k7!'.@PhnيxmUXC(̾7,8(VNYb,wK ؖ~_?l#K0H<(e&Feo0DnK3n2tF<ѐ. ?AG-pu*JIӯ_ڋm[gmgygD?7K'6<E ]D)a|?ro!lOqc (BHGExulus?Bd>&c 2\$v0!:rWudoyc,\/AmnZcsMyBk~Zy.郱&05 dB-Jd燩"v5es˃/иAm5}3Nz[(uțXG4m;lB&G0nDm ,mAOmϯv%I \BN(N{Q<!͖P[1X4tD N {?wzc6M0AA,^'ωV_Y3<1+mκ)~6S1=OF+ } ޹#d_狮pG9eZO :v#:nBaZ.RF0\V/3?@ɘ4X\/%'I@r3koyIJkA`-O@md=OKDxyяEkW("hK"X[-JX6R%F^GC+#oiH @I։qr 18>DFvCm[9Ls0q6{VLQL(2EG>'K#dXqGƞO+)S蜕$$ ):yOi&3Du R,wkj';'a1bN3#w!Q$ eJ|QBDNF|5(X~JܧA6EDt}jF6TlR6DܹK\K`\L$QZf v I$#I"ͩ%.ՋC4)ҏ~bAc >74?M}yHi..dA.B{ULaWy'_tzn:y~yB zV!`E]iX#3iE-.I[Dg30ޥ淊kgdsQ2| Tw^')^jm" eɔ³"89$lkdr9O?O zQBpdw8̕Y'fbfO<@fILn HVGY8q8g 8jW* ·*'*7 o"yσ`2eeYf¬ }Z@Cxqx`5 x~~s36<'3vץ^Xr?`=İZ)O & jWe g^DA5Bqw{ #|_Oy7hJK{1 aˇz^a(}] 3WN9>Gg@~ pN(OWQOT$:P'59Rn(&'Yŝ=vWɎcuaټ%%SϪ vJH& u y_M:h8̞Yj"@n ѥ<>|_ЕOdl.c $P>*y0N::;6Ĝ=b ;|΁'bxCp''s:|a0V"B %)D`ބyGQ={SGO@xNR[N&w\ (% T9 @Ctg<:}:9[{3(H6o0PFvyہh+]w02XNKwpĉ EN u"EvN)]nb蓏`*񰳽pT`lQJtxp {cm()A"R EB$P"A p5}43bo?:!\ H=q#;c}Qn&?v}˒),@T^A}(L<)^GE:~9_FH:EA~''D~D1ȼ'c1G \$)!r&8Bۙ% 5;3 K"@?s[iB ?/1<en3:7P^ ^%np$oۗ'z^|y0'(Gx3[˜f"@x9(ڥyg.`^{g̎O0t# 2?G? I`gIh-Ȱ1J*g눙)h"qA#`^9VI9=.Yf2# e'ژ6؈ZUab?W X Nu?jL='XMDuiSa/btjXsFFtdhB*2s_Z5!mg.ש->zCɝb+dvRR;}p;[}+xFV·[("G+uݠVv$K[O?#tmJ L[D6 !KxO W3g"J()Raܒ?M_|o*o%b`/-,+6-Fbv%_}%#ORI|U a3 ]͞QBZ}2Y0Խv$ŢV0Ȗm7"tǼҿ&t>S VzEy3mf3*ՁJ,:4~p_!px K=kx=Jyx]YI;,*Sq8YC8F ʊ Hrw'T}Nw_GIu$o49I((8aTBx!B[yV Nԇ(P{}0 BvhJ){2 {;=EV! !Ғv&H+G Πps\ 8zBɓ.L)$Q_ӟ#إ'<9 =NSr }cMSp& "ǒPqBsag}=Y:7^_,s+s6Y zUK|kf1x:4cNքDNwDo9 ׃٧)ʾ8849, ƙ&S{rl;?oQ->GB|^F8Kƃ P_뼝J*IȐ"ty=gDɽfAzyV(kOT׻WH"|! t2)%F4yuł(:1ɡTUN1N9:8˧8>iGbqP FDKA-_ ~"x#B 9V!Qt9 '?<#{e?G]Zy59yCL(|+pp#)PZbu8<x8~ p>F8}c^ZyߙJ{2l<|66}#Ҕ'gu >M 4z_Oĉ\%L,&qE9${b&v{fG.aO}a !M+{UuUmjjjB)UWgǼ!EUU*^]o}z֔)UpwQWRi%IPWZMI*Hݸ*eNO;m*tUEAQ;[{zUU*%l"j֧h^wBK"QCWx-dP8 yT" w"J'{VqR @(G^0 U5A!D^ꪪUzPc*R K&@5%P$}BRɒVO "D! hB%X D F>ߟx^o` qZv`AѺ @ ƨ(@MЅNݴִ*0P `h4*(>P` ()@@{0hр}l=i@@]hk  6@V Qhqw` hV0G B :*d$ i$2pq)`q>\8̾> y̓܎|?L) Nl$l >HCHgN(Y΂v~x{_ emg'pDHCesc M6+5Nū17%թځoܛ:]9ϜAGk7'Bo M$sƲ[m.` >/{ޢ2ldH_3JBirܸǘ9M"u2l9sgiFq|ޱcqvb,q6mo5g>3?KVVof_;q?^3w̹ALulfA#xC}\9EÂ>8z?P`qm"n77q/alsۣ4,lUA_Ys3u0eHr8|NŽ?>z$&4o[ 9p="Y8!ɡN7Mdᴋ~}3+=D؆ l-aCnrƍVxq/8%?kq/'c>!7|[LE0ٯ˖wkcnYJYS,߼9ʈtk\\oYo͝lb<ȃɰeyJ|Xwg ))N;&[ ;Ei? w쨇/4[0sn &V͟Āt$p×< 5G"bCqN1t=$H>6Lۈœ6݉Ag&'9v Jwvg $n)êdHgn*1{򧾐ILlCg<6{j1?Ԩ2)!C[ ^0߷͡F8:F_>bHNnhǙs˼bIM;ȤY|eßs'?4qp)Ιf]]|ƨφυ$Z Z*Ob;17ܘqÅ=f`k_wDq8r'b3g7RTsdkm1sM9d/zbgKrt 3ܛ& cZˌ[I~zox)8k_̈́ n/q+(ɲK7 =##fq?L3x:OZuzޜS yzؕG{cg|s ,&5y8ܡ̝ҷkFW 7uh1 9_kɎ||c2o&MC[ng3I-x~b|ow͞&޾3+*7?;1~;{ I".Tdav0q5HB6w{s/3g9}ѧ!O]IK\e)5@P%1].jȺmn^bjr쨕 (xsz ؀`qfFkTT|!5vUz4gH֌z]MjMOUϢV#oVjhBO䓷3&=HPXb L 4ck+P+ki4)jK)t/9c%zЮ1X MIHOy4L% `aLyYf2̕fuR`I'HzgbmcHp]rȓH0b+GkYpσSiR y$=D&k]o̹QnWCsDH1QQttBg|`IēDXFG(+TF&y2iz>~Vt] 5fIC9zHt=QUԙmFӹ U'}Dդ$>J.fru>J(ڛ/n$x\L>=D/*l+%٢jy”UK>%^1YOX" 9\L(͓gMqUuq~{IZ6:A dO ?HHCH6e*HȕŐDh)/{)>ՙI|zT;qX\4#wy㫈#GVGCU9סr I]lO"l/흜=:Dp$c9zo"+?Jx%6Lm}d\b֘R؅OOTUDH?!7z@EE#kҹ`MdN__#Z)}/j|~*WbyWA'dl"/r,*i6ycudqEX#ri~,W0zQ5~L/O!)QA BĜ Ec7 %$I ,粳CAcX[+  U`D]\([ӽ*1[gz4 Hm=%tKq8f^S^J0uз)` s׽TŤZ*Eiz(0L3H˜rL=Kn,F#wT#\G]ҧ]^@z9)kW" (d/&f ͐S3ZK?̖ǔ̯e3>a>c&&bX'-DTRX5#iTJ`ǬyhӧvYΏ5˞].6YYo1 }ܧ:\PPHR <<%v6=vlϝ)u+l+5<@mT:ҋN!AR`{]U T>'؟[>&TJEl"a ^Og^AAO>^tN^}erK淞ۣ+r%~zB+odSiH£rt9Ⱦ/s'BM?EJ+Y1L:$~ztq\&{zeJKb)S7O|P@E1GoT!(9lr<Zv4~xpxQ34˔Wis7xsD הDI# ]]#jDN*qSVpѼ^g\9%ߵ#.0<<<<s#OgB&=='Ȁ T0~zv{NXUrO} :Om}$MKsy9=X뗄|Sȟ!bϩK烃 ;4¬[{/9y^(´`ro<)Gt{ٞ o*_~WЈ}h{x!x`kǷ %`CH@;9a[u(?|ԂXCADOooW@9K 6Cjy@5x OPj>GՇ~;iE/e(cEYaš4tu4Ӈ˜IH4 ΢LY;y 뎸y|shj3mxaSUP1p,FZ@z}g|L>f:>΅E< \wyg\="BPC^CC`|87?ŇrrG^D,+v667Ml `[066mW'T&zֹ,UUU\0yaq%:J; K{pzvĸ,hʫv zjwujRnuKp;v 0@0gpHDKD)vT(TҜݩP.@ ("B]>o0\:"B Ix7J?hh$' (ɐ2_"T '#>Ǹݠ}(ѣBhc>0@&>0G_vz|xfxc h{ @'>14ƀcz ;@'c{4h;{Zƀ qi;}4axx 4fG==ƀ4}4 nǸh>4}`9x&G/7o p T{G`>o5@{@xpk@ ` ?=rY$e ?'= ,["~[3Ѐ'̟s`X0(@c|̏<ԣ &! /yqqAglg}Z\x'۸س-t)uw_3ڿĪXYlz@Lqa!@c/|4:;fhzAZ2gš;?s^wj\RalgzXn̏ӗ属hieG\GDZ^πG;SQ*X# W|ж=~w| ?Ձ7yſל|0Bs24CAt!ϒ<"iBw2G8/OggSsBVX*'.9׋A>e{GĶ rlS>&"dҒP)No;`Qny4#e-?C23 | _!s{_tGz<.O B9O|$y׆@c'~w -:󯫷Κ\x@1\F 笽HՆ8~0$=ǟy{Wx۽ho"eG&B0χAsS2>7cctJ#w?3,$ "VļL xm?{͐o;ɗ"q; M?Hg?-J4tRIȦ*;s]nCx !܈Db3ټ%i#yϝ_ٝe4Ggnɂ&xyp<01GXe%:Ѻ=$qʄ5q8@~f6wIKC6|~?[4ADtn[FA+:{/mvpQ덩t-<˜XJzHJ.DS/I I(#ήj1*ި_<9ۥ #("_k"t~OdaJ4𤄟8"#8O(Ŧ q&}0/RhJoD4IUĠ$˂H;˖CHJ- ./9K\y`%~$Nc߶aD"KFsnvf)ܳ@v0$f7Ȭ" VG ŠZR/j0!>ClQbw>&=|vܛ_GRVCT;lw}]q g.{~drfEQ56jRb3$B=Sɫ_I8_Qk=[QGU]d(e<*CN*)ȞXHp}ߌnצwePڧ)EeyˏkOGC N:_#jqDv _ygn{G-96yU!u7̉^u#aA\{K=R]|$5.o,9π'KH~CS+#' ,YB ;8Fp${G)npR|UP<Ji+4D.JDtߧ+u*NxpL,R{xb&KΨ8wɓ7yz˕E*"sSySn.1y%9gYf%nwn^zyNyh{gyg5j,/0'e'7>Bι랦mtۧ\\EN! S=&>Ky9&:Ff(^x4-< <<;ځgA"v{i)NUOgND_[@ApGC^ < cA&ںt@!NS}M<"^F6 BYr ;F_e qZx6${>t1 ,9;9c&wpieQ[LǼO⍶[-E[af 1Us CV=;[ *:E=۹ٻ pf scdw 2R jqg^2+t  TIRGN78Ï9ew`TF҂vFX9q(X%.J)UJ%RJkك#JjԪ6繤Y#5ffe*,836EzH G}xx<oC{Rp%HGo("7tAKQ="A@9%8?鄏)?dy}㡡@x٭h4P{Z. w74v;B{7>xqӼ=ƀ>4}xyyrx x4jhW6}5Gh}௼hh5ڟx>c#@@Ƶѣ@{vѯ;h#G44}7>x@` Ǝv ?~2GX_ʟT{}۱{v}Ahk4; ~)O$F'! Hqa=Gӻ9fz%ȿnر x~Ǎju.+MD<fVw˖q-4nLo3*);uAy4ZŁk;=;d|[%/.~\f8u7,!ޠ}n7x5'|gCcHI7cyy}|CHw.Yǰ9ZTAb >ceje}20P$*)0/EHsp 8fu3$|qHSd8?.NϝnJZB`\_QurͶ]QCo|HȓeDY^{O6?ʂ\-Q`w 4>Ƹo19n̆)>v;8j3ANy΄L?ɖzؽÇ? ?ljT3ao?OggStBVX+l&c1;朁Áʅ\M8dJCHؖ4EØ; ާ2AyLi]ZkgK,5A}1sUm?ߠL؈eGߘ"FJb왉9NiN"w"M6pC |ss{",FTm1CmXYo~l nϤOpN6t4߿KJߩ]t'7ۈg-[qjr+aky*1fc8KY\d QaVAlojDi1?=3 |}5 ?<0M7@8wSJ a7O.Ɵa'gXw<|u<4v RMw1fӏ=S?.4_ަ1>w„!>a`qn?.˅ "q=ːyߧE2u,-k_èÝ@\&T}L~:v~ɧQj:o4ރDXp/i(t\ 6~g@jnaC4ǗN h6xxCgny}pӽwHF>y)ˑ|A* _זgpfW/ :ԜNjL QvϸtDGd}U|b!$\O$TM쎨.ۖy̛ZLOrU`LH" $9SW*WәRTW@q0؁uHG ]HP-}u!6F 8ȶ%,d\*|*HPRRE#t# "l{ZJOVzNkOTHI{үW)o/, FNA~F*0"uYj!}Q,V?mFнwpR")B8$P!H!kYEQJG9%t`e}JкwK V[V S FTQ<"&,{ckjb$LB%+%} )]@"F4(LWSQr,Q)hզ0a([UH\Oy$ )cv: 7|8Y3)GcQTYiAaARi"Hȹ bzJNXlO`.hcg4hPv 脒(WIjIIQq%d sQJ^ J̃A$AFQiB39OD\}4UC5lB *sbL(ukRGѫw(+$!*)~ǃ@Hq<,iTM$NlE-lvDuة_t'W$OZĬR"=6 BJĤ!='/5f^lNOd.Vkl GNqbEP ڭuI*=uRiE͵~IE%64˩M($I$($J%Q ȥe$Rͬ6X eq$,f[k+daKcrBkk,ެHe~6aOVUr ՒU`yDIqj4 ˅#qB iIH H$V-ky^R4V6 $TR GBW(}E +)<ʣrtG|r;#x3<U3ʲ99B $ǀc5.}0irv:=\A{CJrv cQ76D rYo.8@php Hc<<Oui7rN*{"MOhN3~u3XO92պ ^s{^/ݹc3G~|s<<<4qg0|NON,|3 1S'a4|p0jp48=<5R>)|`|xzHO.k4"ӽ>0AWkᦍ袦hʁlD|pa8N:@קV*Or#ȅ!_TÂO YL8:bX$<9+L>@)t'L<@xQ&ԧ7lMeOHty]s|_9> }Ao&W\;:ķ`q~Ϡ|W;)D #174Q}t`ӣ@/a g/;M9dы%N& LSqF5BIڑNCyu<S >IxC|Arq98ǓAqFڽ rS76 ^qjP sc,} \"dq*0hrB~b xz"Ey:!QsT xv 1 D'U; )GAmՙYf:tc[; mmݷ]mpIqm47t&(1MnNCpALl]n[ ]Һ\ XͷmYsww6Vmy:s$m%SYm`l 6mc`osmق@f ͷͰ-L}x(Lg?9?UI {$$>*u!9Pd ]XI2AЄ)Ny$ =:JIyȈIwxyh@ѣ>i>iG_@ x|h4y|hj>}xFp>>}>78h1v7}}㱣n{{G`>`;}h4}Bx @_~ /xvhh4ݴh `>gj_xə6^bgO[B~?}"/;Pfat||#ԮۛFS= [`lߞrT6^M[󕸟.`G*1tgkn m/Mcs_]m3  Xe C(_]xsh~g0Y~(=DK3<cxh^vė:D4G]?> q}9ݻ=lz:PD3N?9uyz|jGfΧ4( ˦D`,C(Jl~rX ^&~ G"F#W7Q&D%Fs6Q!7vR An36ڄ@?m<|lDeG8]oF,[+8Ȱ`qy:s|ͥ|lAlVFr5> >axIشt~8a1cO!Lr0'g~DԞd=Fҁ=CCp]$[-3_eXc-!m~x~AR=OggSuBVX,Yox΢ B>~M ;2B2:hX aHLwL[;m]22>N磥]ڌ?i GW_ XS h˙sytnmb3mb6" Bbdxk36C昉DFyx%LQg`"P=s0'aw>wRwcf6[#;חm9a'#m{vg#2<v9fLEDN4oH3Lw#󂓩<Zt(3NemoУȐRI(Ė|uym "#HP.&:Q?i}asȎ*ο_$ '^ ߯ otsī>I8ΝYO0cY?3k 3ٜ!$(<>l!Zy-ȴ#<װ̮|<:6ŶLgp[+؏Ɉ'hi;C/6 N rKT%RTb{6AI@UǗ!`_B}bB0H|a*J8Yqj;nE-6Ζ8Z=- (M8DdЬHz9Z!DΧ|Rҳ, )nS׮C(\ahTp$jFc;u~ ' _z>P֕(D!V ,ɘ"*-2諦J24f= W,z9DLDA4/cJ X]MK7ĘD) &G>.^46EgҎ'߾-:_LTBީ(>kA1U Wa&-e9;掟DN6x_D8Y) 5OUQD]/UhTSbkQ)f*U&ҐDjRGN$gY6#aq #p l%:z寒lXuE .i-YoZe,Eusʾd'صUYR+W Y=rVPDHA[0,I=HYuA%I*?__@摹d(]-(JJ}_#Ȑ%Y'~>ygma_I藕kfudOY\f%u_Y_+w>.jdrY\ZgcVece<ϵJ-&p_` ZҲT,q:@-I Q?I+a(6ɧ~f5s"Gn2WgLǀT~j(,dN[ [VNmTpDWyYw!3;5*b&'J<nbGz,f1znJkU`J@8=[x˥kq Xp9D[PHDOㆠLQ䬒rbxܑqN/)}O3; {T՜'Fs@z5~QOr)6p&R|8sm?698QIf%4y_\KОDzp^i9@]:y!LS=&s=bCLa4)쁱bG=bӁ:x(#{{ӱ>W>~x>=; +}>x{xP4 B@5G߀~ )<_L-l@6nh\lRiWmO0F Ө"6Ǐ=܎`(bO?6c=!lS@geY1#ku)OpF3y[?|Dć[3 bO(?ÞֹM EPcc0J 7&+ofa1:$Gk(W/,plvIڌ,ΠRl6-0>:o9omTd?9ecqA/~GD#.X  6q,]u!?^'̶ě.y)P-5cy3,1xJxBӟhCUνqOcy_.Lnx ͱ}LQ,؀í!]Ng^gYa=|NgD<]m`Sy?EDn/1o+#o??eYnDec!x ۨX(?|M_jR)RP"Ǔ&K9+V]^qz|-0Zʠ̈'pxF$ 3/ ={ "2ת{LsV0zL˚F&[Y3#PJӉ$H5l4}bbG*+&Y#؋fr1/73\,H߫]PQv%Kzēl-$!-@}z\ccRYc ^YFͿqЪ FFb2m/MVi&)zmދAZfH1۳ѣOggSwBVX-% 0ʕPAw?R=$r%nʄtQn~?s'_f`Z]$VL+]HŜ,Pzq"Q^#a.Ja`\WOge$O.P ?DŅ4Kh%pEVqbg,={sbx㹼qeCclڳ5PQM7jxn$ZSVEcc$~9Dr!;ޔ^`(e=qtv,>QyR^g4 S/i0ȣx=>4G"B96 a{f9ϡ< }I=ø=m3ZpZ/6C_]Ss{Cxe\MCx?SupC&&b_'py6:P}O*$:>nBF|TDEX'Im>3o~Ov'+({?'@|5>3>TJr|Ocw†}O1m~iQX#!wdL0g/R!rBހD7UdyO8Ӽp (oѼ0{AA_AN{>3Bȸ4-+'t [jMz4 ݴ%Ztj m]{RuUHHj;(ۥJ:uu(=fꮬV֢wU{ꪪڪlF*tVJjMVgW @IA %kCG /*Bu !PHR+)\ "HR$ ⠕ > x>Ə}>}>xߟx>x{A@~N#@փF#~ W'$_mеSۣ1H8̴09>?QF~(s\^-?[ynlu |V~N(6s߭O/з΍7 $~gR,c'*&+!p1C˘3@ yP&=ϒ|瑺֓Co=elgfQDc PsyQI1'S+O9B?5r~<#)0Sf}u?ai%fGſ?)˞}n;`yr<?96A Oϔ,!O[_~DI?xnIJHSex-3x~^#Gy*Ĩ%DvBݠpIK wxą nseX`|Qk73M8mQh#s u>`ۭOΈrLkKml^ Jև8s+BuhGsk1~CL~#ЏDա'v"' ϝk෿YbTirFJ%qY8)R:㯈1“g7"M~u~KL$y_֝jDf!Gı:r2&Sק}GƽK͸ߓ~1(F|6 yݏ޽i~N5}Sћۼ/bFw7eKzv|%lȈ4y4ð(D 1 io>$bfحe?G$]9 Ν\))js9ucڳ|a0Gcky4!wFqPaaX n10zZ>Эa)-=*UtlS$bw$<"WIG +-}ldk ~M\3&r6HGmDW,BPvl|qu ||bbGL1L.$DAdRvQx/1n"K7άƔNgEX!([Χi̭~IV.lOlf2E <rIRSA&b" ] u1aL;nVYXz}(E|DLE!3$*h}H۪~%M 6y=t1.`R^_c2*pzg/bKmKʘB-ymg,#h\WՍlMu_}$,AQ6#9=Q,FLʍͫ #y[6LS0GC~^VG^뫩A4jarsʲEpM"+JAMˆ@pGl^LSBXlԒ!P8&8θx2T8ɑg2*Α⛒mUgW)W17%c>6y8.3i-'<<0RvPoyݽS*SAD@!̊DpI~OfĆ=ÖqdžN?%<Y >Z|f?::<r{y<|yxC>fp_ɳ7nOH|?N)CO`/Ptzwt-0ہSD"OG'(5 ɉl0rÀK' s8'JC 5T@ &0{89;]bye|5~gt>߆O+`ɕ51=Ӑ<~?gH|8?Gބ&DNL9HP62.GHDп0$`M7ȟt|ir@&DDc֝rkiy#r'ą3'I)B!C4Bx$* AR@60:6#փTx0챡0-ٹ30ZB)`  f{b)309b(N*>ܞp¢ uH(j 'SA%31ju(T1@RA@D1@@(PCx}>c`H};}cv@}>X hߟx`;4}4}yXh4h}`=v=}GC~/{h>O a1জ~ïYD柞i? 'xS+ɉ2(Πn SbRNw?Mv ]p#_ N4$sV:Q"^/2,p_-$fO~pD]v>u?# E۩4|f}$91{fZTG>>f{V|)puy:G30Fߡ~wQ/<\?\AX k/LzGN~.;8 s?UNz893:˜Cq{f(XB&c3(謢'Ezʌ;&t OggSyBVX.7a?ȿC'R]XsI1\(;/ynP>y>& %qֵ)C5g 9>w0(]PvdOy9 1?|qьWa'G .?D9$!96g+\\zh/*~'}KMP"5x]"}NB!>NCd'K'TThΒDtNs$z8' U5'kIaǝ+d4͐{QbGS[a [sњd.ȀOzװkG\ {X[j' P6Y|ғOO"㏰l{Ir+)Q6~l^u\D"D]г=kL L<\jdUHȮS+l#tc1$] ]J a\1EGAԹ#o @E3$)&Hc:hǀ"&|*8.A|Lj}*k"6OB(<3yצ0it24`KMX&%zH t9Y5rěĈK/;dǥ%_Z򞦦+$mEEUe%پ%T%yhl*1M4129L dtǷ8Ψ1 =QoRC3֪+75W3n. p{8y=M֨Y+8ͦQsf`۴ac6j2xLjqսxM"T]8c 5kgc99@;^Cy['-y;a˱9iC>"1 р'o ԙT p 9ZPUs7'<2#AasDt"Cp!!(F[M 43sOP#352vy4RE՗APG^t2HO4>]qy?qEZvdgO|C9XLyvq58峒, RK}N){g=5Ga .5~ qHat| C lp%IP$@$"$G>x{iv?=Ǹ>Ǹϼ}{]4Ǩq}xv>p{@~ /}  ?2$YNY6~D{ےR._쏎\11&0~DI 0qF20@-{{uoZ߁w:9l :A 6c˒rBM P#POjɛ1!5^ג'x 3Iy2u B73쎂#۷Q';Լ%{Dd1'ʋ<yz?3~u9FPqC?'#]u>y~,ǟW?.ٔg KyV͢~g?pO[[s̯)Ng_듺;udgܹ/O_b\ǁ?_1+#/rxF#3c$) InyDy}ɗ9egcwT9Ĵ!Eᾲ]eh?MOϟ?nKsqg~'_3R~nw:G03o;F@|ENE`}m[n{"cDmF ^WßY 0 e~41 =`;i9[Dƴ!1SMϜL>Bm{7#"Q_#pb"ΐ?ᏽ.F\ltG'~H& GynxS)=Lq,X6\w#=?3vz n18\V<29j״naB^>]ރ2xC}hcBLIBu D`z#RN:@' OLup1zĘS={`AJYo:Q6+Q2L)`\1!ֽA1c("Ko(3LI4z`OSfz7I7WZ]'kcW=krV)L+H| |%tǽy0lAJZ%JK8+g 8c`ppz61:D#65 />콽l{*7=P+?pc~繧-ep #a71XEfnf\a+ KFj?7CUz|<<\tyy瞸yyyy>M?b>Gȧ?GoGߩ>?<'_;E qYdp0 t > _\J{>gOPD||/{Wh^wxx}Q8 \Ny^Zv$ y!>_UUVzB( UШ@UPP *@R(<(%QR*UABHIP (ERT(  Q$DU@UUV?ǧ?ǘp 4Qx^=}}}xv@}x'x=݀ v^ Pp (7~]-k%gvu-׺ɛu8d%lg`bZ!zcZt/nsZQgo2րZ;D%#lng!n^}݀@h } `;4}v@P@{v44Q@PP@Q@@ 4Ph h4@aۭ"*KQ2H2o1t `b|%$Ƀ$c7OmH;3/ž C7< t]h{8?u\ XA :򎈝:\Q$wB6,{hZtpP6.Ѝuי8|A mQy\CE]W+Y] =z|/|PgQJOMɪOjgN|\ 8Om)Pii4К)M(O_Mχ-/B'ӏ' k݂'KvqSEhb}& |hлPOޏs_2}SK8H'ذÇ!&aP|M9&hFNib>8Qhh/ r"t`'mg=)}w^yɞ(}}_& By!`ןWd:KԚ.#jٜ 2m QKb^ސne(IFp]w8ai4XO4mjFCXH R e,$ʅdK,mYnzxLC9UaO'Z/ קɽ[_W?n6kc_{s[;[ݵ}_ɷnl 3mdXe,?όΗgٳGij{کLx4WjXi }6D}vK_۞BgSoku:}EjN1>]I EWGfkw&vj@nņ}oon%/]izȾB;`?Yl/]wygL볢ݷ~])A_""ԝ!šx`dęSQ-tӄ5LJcٻ7ބ|S]ݒX`7O߿g}_ݿKe`v}\?n߶=yJ`Wh(h(q}\1e:"rOWx343bwgكnFB_n%߶oj3n7dٞ_pm&'Vٟnun M<|PΞJ$C.$٣ ?^3wdoVl=LjZlW޽y,C~վ=txJ7feٳ>Pn:_d{t /ۿ>|ygK ݚSv Jh'˫.)dط"aCm XWf:Sa,tmX@g>"2Rݙ,2E*nny?@N6^vg?nN1:Ea U޺]("Rub}5{'l,VfJƤZ@eu|m?"_Xe=|Q+FDE'ѴfE[m.' 3!8؜']:K|:"N'L"fpW$'HzD|ɤPu:o'MӳdU"$[٬,ݿTFց܎nV0߱1Cxx/2WuoJZf:@YXhu 0ءde_K BU^ܽbōUVosgn7b*:uJ~-脳:_X%ң@ύFP7!ᩳ}(NJCcg2!:3C[öf`C7umf"]1+u^*atP$aDy/Ek"_kLj`*ۤ_2h+;̆`y\(fƃWp<.* ;+<1+0 CK7CLbffKĆ L3cv!wO 3!}44`0c u1 [5E94wD^#?J!~L#Frdk:4t/F0mwx$ HO0Ct=oKpqqFӞc c=SxO//"q5N3w) S$En~(S )W!H 1!fN4]q,8cx1#}m06Eg+qy;DI53xG߶^\!;p#hy=?~11 MSo$,w"( FPDZm7DSEnr"< |3L3sE.(r.'bpOzkPwENB By:t`rTDjyRK O)X)㧁ly'A)ӈCp3Bkux5M'!޲'79OggSzBVX04WiKrBx4dy~CDD$j:n/7AIH*tlTN+U%VUNf3hq7NsJ&5sQ1v?7t A(P\8)ml7AxLhf -݆<3!ٙIwwHf~ M OAݺ|CB|v\.A2]KtBO&YgUA#S<9䃢Kd$!Ԙ):%3;SyVJce%" ߣOz2%U)`EQdϽV9.D9-.O7x?NiFs$%>Dj SY j ?kA5l<5Z@@aaMZ4jV8&5 t?;85 p ܃h oJ07 H;u)f8?ZL]BB*,{ZB׭1kֳ $si NpC@QSP;!4h,)PCkwC:Ϥ)ќo"G2^5 gjEdT:1|sh.Ez9js%qfF/ѝYgɯyVT8zx.ck[ӫQJoZR(ˊ]5@X&|Y!Am7AP(;ۻu.y ,V[vmz uh@Ѐ`w@QVwmՠTIU@Vh ޶6cc7z͍llllllllA$:egolAѫiA^AFO(\[˴&wUoJԉNV ^|.6^+nƽf@=ַ^ES̞7Q~s+–{NRfe Sr& !33𐛩Bwa!B$&sR'Ua!D"7 Sħ|YZ˙ҝ괦:YA ћ_9)aIړ*g{z̉V8 L|J6<Jt8ZB,-70 Ey\`"\#d3vpDOsҦgM\QKEbn4d%%'vf)~fϷdK.ɾJd$>II$I$7m߲l kKit"ķdINbI$] m{$)$DI-΄wvt'3HؒI$IwbnI}.2^݉NIs#@7AKSס#.GPk pIɊ-& (J#N #(Nb$cGl1H s>s6{&sxd~}Wz<>.=3 ~Oz߂c)G>!7xR@@i}b;cH|8 :T?9w)$Tz& L=C.oD3|z:M<68a)NJxc1(ks?|to)_ |*;[E>G( :ty>(óΐCGG^yc%哘=ߛrΧ ϨNi @чM:-== x;"?{S OOAKOS{F r\ yΨ &&GyNqaHa}eg*oުꪪUU\ǽ{۷1m:] ={»8N p <{{ڎp;\C{ws3c̀6.w0tqa!ca&% xU_}}UUU_Q4DR$^t"O5:kΰA;k=XD݊E@4j?1}}?4F>x` 53C?Wd-~(k@ v]>9&11 l1~Q~Q;cGa|(qfA|yH@S4LЕ Њ[?%uХh\"i:%s9 ^T*omr䂹? po>RfZYLP)s,8 |.mo-l#˿>x?{sK?ߤ}ɚ=;OP9}7Α&~鳇4OKKT#~>HO'L |O:M`GS*iGpi(S|b~ߦ4pw~[ŸE/hT(d^RUJ;NT(<ҧ2ү#bo4>лaAגe fF*H]b].y_0$~bGۡ]>I &_ &pN_`P Ÿu3:fbby#Ǵ;/Vk艉[w8Ao09Ў1aC|3pq~}3^izրҰYQS$icM2Hn/3/-Pʼh# kIk[\q{ =:گDuO/AjT4%3tGZ^$Udn:kZ[}4[{݊$=8Ti#CkSF7qlgܫJ´VUk ,V U|֤V_d~략 Lrbۮ5>OXו^\}ڜeɾ ڍ D*ɥc)hDhP$i#]ƒ4q6mohF:Y]WXQ$.i _jk~MYRO9"ϙNz]Fgru\]krepUSKq\##MQJQDkYU'A7y{߀;\Yϟ67$ys'&kUZնgY{,oZ\܊1:>yÇVݭe|Q TJu I͑#z]VRgdoИ]rrTo:]N4>bwQΝkXtgzpbU.V*9R/)9ɞ vΞEVFtz<թe|mTfg&[lȅ ^dMzp6!{W4T55 `ĤM=겈:K&> -9OZ_aaM4ϛuIM80-Ji^kWR^z13JgRI$EN[¦()BL&S6*j K|ĝOz?J)FH%2<QA"jx $ׄrEȕd>Z<%. ?Ry )3^X:5ҜFWQp!ؠޡ}:J$wZpñ%;1$Вr,ц`|zp*ªpÀ[U8G741Vu>PU[2iwTP^Zu[kZo:k+(9-:m r6cmn PpY#!ll @dH'o(xDbIe" ]` Gש'{qNMog'|OEFC<=aۇ'2~;xYIy>Y{}ɇy|[VPHjo=*y zG#pw)y\_=d P~6 $=^L MM'\}s7|P駃'om41ϓ8,ȝC~5A>x:?ȟƟ_OC0/{qGcȧ/ؚ ?G|/z=п$yq?#yP9pz:zD;DD7 Gy 4`A]9XL>; } ؞Ok:>Ȟy5M'"9M1 xA8Y;/\'jE^|)t8dD iSqG\vó |F( |O's6mmm}}ݳ9Gw:.@]4JƁ.ssqwUHJ: 8Ppp=]q@# 0[ޝw|~g8cxG _fQ!ʉCDEQQ["lZ)YpJ` g oKUJajnӆ鲻Ft\nꪺXUu7]t*Xee}?q` G]}px{xq>}}>x>}{Aѣh }xxx5}~ _8 4ѸYҾG6.ϊpt|]%pL}֫ps"%reKv0yQ7nl#ϹǍe_ƒXPe15#MQeVu>~WDY̸?BwVk|t"H7 ǼmȁpjHc'qLH+?qN8d|ę RIe~/g}40">}?hX\F293]:92'D?e_b|M ~ODy^_є݁n!:s~`/<hD/akN&Q?$/-&<׿c2jr|Gy:߷wy*'+;z؀YNowd0=f1A2 /r/. |> ZL}efsy֓fQ1>'뛀 cy!e"%`0(Y8u["s2OPP;x3|K;F'AAN=sd_>#sP3 D EO̺7vNz^q snҿ7uwӌj껶>$i%~v^!Pw͝S#!O=#)׀qqaЦ+%]aB;twй?yd~[؏aa"aD9O]c|'\D/cڐtUBPuT!x_]#:9xs)B"3PI"J҃8y51^ R~,Q,EIV؞ľďbC bn&(p\.kXׯ__W_W_Vdw|v1Gl"3#\ɑF05 `9s:Ne|q珏@@p鷜+ GO Đ/<':A^Yd8bNgj{{y~ <tG{=AR(za ,zh|'8;@x6NaXN>H,U~e8:?I_$L姌A;yL瞧 7bCS7~蜞ψy߄>~%塴7ώςGz|)ڽSyC@q)P !-=X` ti9ȍ%Ml} 6S<Ձ=A4l042 ؆0 x͡clg= :{{kޗ1oQpͦIr&DPj1ϬcZI)a2T*ń^-WIA,`ADYd\1ta7ű&b&( T"u]TW*QpXqn>v4c=ǸxǻA{kv~O? E|@ }N\~N\+}[xoU~ I~,R[0e'9B~dL(YY[Gm gvVvexk @??$Q"\:/tH'Hu=,B>: yk!Ge̜vWH0~y=Y8 3>t'%s,{K}\ᚘ1kF OLg"+>oܘ9[N.4QHR 'hXMIt2Ylg!E8[a6=؂?''6Yyȁ Q:&XFzDxy$ YbAF?o #B'%fyܦ} .BG&O1,ObP CrM y81 Fܼ_>]~H_Ɏɳ9J#d/]8tb% fsl)Y'p{`*σy;>6?GMى'?;P1ΌX|y<]swcN\ if۳39~wc.\^ߏwgF.5տ_ `NfqdRԱ9c.KE@DTJ^,t/v!+ëiTXZ7&d :!&fMW"mrp.RLܢ2]_ V7͋\$RLʘ 5*T'd#1Rz,i7y7ʧeZez,iF!קc{Qk@eCVTc%С腟be)~qP!xkB$<"QK@%/gkxX,-e BLn_8DpDL#"\"@ShAUQy*bJ@*WP:u"e<'ɘMOBQ! \pE2b9RFRb*WV" ErP֔6*RI C[㰛̡(@R9sr؊I)\c2 uuR $ O%%! PB%|2(@$}8`\ [_R wjΕHֲ:D^+(#f] /IKK )nNdk) +$XSMdqTbg'rs,ub۟v,Y9fA`9F@a0?C R̈́!e2p<<'*xrOʼe)3SIg+YgNV2E94k+_\NIOT\o*TiV_ whbRQ;pH\;pObqpb{w*؛N#Gb;u4&ጎcэP14G!4&ɌfL}}wdqdqgWu(6-^pqZ@KA1/ȉo#^,WEDc%Ud<'8gX:~$Sj#A| Q7tב;8 cZ>j||sWɃd9|Ӌ,Wer2'z,S| c y[]_~9/ϜJ+{\O!ѧFA$(׳O08=C0&D!gz4zAž2GUχ=63J|NM~Nߡ<>CI9:%D;4rxbzyќ^EG'Нp'XOYXh1,1Fo1Y3oG1s3L}~q\Km;c%>f/X|sƚO{4z3oX<iB~à 81dt;, Țb9wOggSBVX328$b[GAD!*^yHPy瞧yz#<͇#vyy7y皿F'b|fҟ' btr| a!J|LDWBRJ*UTUJ@*l˺ ёz^{Ǹ;o.y\TnwwwGp{Or5ws{wm {nw6;zs\ホ~lpüsWn當[mkmڶ֍ F. T*ù5Vlm;?p jbPd؊&Uom@D{¤)UXlBkkuSw۾A,$*7pͦ%@,aT(ޫҤ"!9@⡄^m~뮓Y(z0RxԨ*b9MRrM DATB" F" Bq,SATpwSwgp0.PUӊqaߋf55 B* ŎjXxǸj >`c7x֚߁ {  O}}x>׸hkX;v54>v{4vx h>x_{G^ g*𚲶dI Ԭy"F?y#|~7zS)b3j(55A@mpYڪ}ȏ~)<+^o9!)q8p>WNӼu "S1_h^=7F:Go |UQؾ i㍌t81 $p|>H~`EYy^x'νk;:N-^,cUK>^"O` i坲A~}12~'f::ޅ͙>L獢yltH<,РU`/3 6'eg9~oٖ " 3}~q, 3}oGD2x}K A NFYof~V#<^;\]'<uh/'+#l͏P=]=tO:Ko2#0LH!/ LL@ ,H!11 Aޛ!Fu:<5ZR f2Ac8jg%+]tDazL!43eȄ !1Ǔ;(%CXmHb;vSf3k0t]>Q%CsZI~3IU.@"pb1}C m>3@Wa2 `+ y!|5 ø]Y=IgIerCsʡO0:**Q1J1>חC1ͼRLR!I'T($ɋQ`.ZiW 2Z<\X !^8BWpS2/2Bq+bm2tdr *%XxDFf!ԙTYHp#DkB!2!:1kV\adA$8 G<2T)|)|JZˈg`2qO&kJQ)l(j2\)?I3JQ(gJAP|Ngx4R^B\HC(MdjyUE'2PUNj&'ZR-ʲԕ.)i;IT)Sʂ2uQAewaQTʚ3I$ȴg L@ ]eܝ~LK2++/1dsMbYIQm!ɭav/Caa(i` }!ɃUM-Ǖ|1qYfBBBEf2"QYc5.H*ȈH " u@A$( 9ǂO<As/e^r},LJbS쀳g$J([˽M g+Y8=F2R%pɸa!Q' wuwYׯ~{OggSBVX4P 06իW_Blw Bi Ϯ>>z>p߀c|201 *lX';ń.D0p/⇩CTG>@ϣ i9{Ey3y|m8]N% pTt*t2$ŭ >$>lǸ4hx;x}~x>'3f~)_ShGIU]>Wn :|?׻y-u$>9؉%N?[ϗm g8yrw?3oˁˆ\90X> 0s3N?<:L$Du8̺ \ӯG&NF͆BB؎Is:~Oq%,U D1~E4'A_E\:?}'1?3ΐwdSau l2 E@#~ÿ32A~!{xuB&p uA3߄ A*~?= O?FF YQ?v8Px`1:{Fȓ<"EFsx#;Kw7ZKq01( p,Y? G'M=/xB$_ 鬬o 1~qy>M"(;w&(H1n6a <zmtJ\IK 䧨yt5Sv_mq @L|sL-c3,8|@㴄oh~~-/i_c$`KNpL P$ $z-K1J*)ة"2 Ҧw/̤E;Jtb҈$!IV&2"2ĢfăJW^-%ddx2i TB-LJ$%6= $莹`y1m GdzYLy)NrNX1EN 1CtO3OTyTzJDbns%iZ/+J"L$j9QJ 젛itƏ,㥀$2(rgeR_8)X Jӵ0PP\qɩ>$`+9nF)M%/Y/edKk6C ߎX11^ 9lx9?MLI/DD_GH295 R\$qI'*EG#Z:7,UGzsVDvS(኏}=#OggSBVX5!-0Oq?~}}>;y{7}^[>5SW}ᄋ}]>"߂bd}{cc3L{c}Ͻ4 {ڷgnXK=2>|>*"^|> 5n{~_xx{}xI.z=cdx%q47(i첾z7yF){O9^ڼ)O;>x>x}>xP}x>>1}x}x}>}}x>B%:ǓjkxfG<u[jB-s5:Gzyl>i}> >}}>>=^vn{Ǧ]ݻr+ƃ}5x^)GON'oJ|Ϫ Pn8tjv;ytz8;/#rc vnֵVwѯ_x4: X*h89 (hrhѦ)an}x.qxnqOx.tx.txEwxwxz՜xz՜xz՜xz՜xz՜xz՜xz՜xwxvK8:]wuK8:]w88wu86u86]wq< qf/D/YǁdRxa6>}wm[7c݉`ڶ3;6 m` vpmg vpmg vpmg ֶKynB-ӵ#VV`m <#Vyu{ޏ<ߓ>O!ZmLg[z c?wkq ݅ >Zv-j,Mݜ6mmݜ6mؠn[bl:N<-7Oy,*ΏǏ6./wDPjH0^(|!oq5I]"(kQLa0śL$O/l^]]抱<|f&IĄZ~pf? q;Q,j[4f>1<7y1<ןS]uÀ Vǂ L3Uޝ9&RB溕?2z-8i5O&_@FW2}G&AJ%'א=g%NU45|p5t-8gp]~\_B}L>}6ڵIqĻ\!\& k~z0mXc /ٲOװr66pwߓ3ٲ]wB`ŭ7-GcTx2WM|X_VK L" 8b +͜!x.Pv >.Lb]q#`@ ʏHqHAtMaLĂE.cyᓃ?m@g Kz8>M 8.zCOۓ螌`02a`gctvu6[Rqo {A7a8_p/gwQ‘~R`5zmN@ .g?]v2krjQ;s$ZO6>c?](-81'iZV\RQ/TN"㸞@ql._Gp *u8#8?(kiY 4wus&:UETpK_`MF3z4jw}Cϗ10d{g81L%``GpwV' xxD9jD2RӖD39m`#-G.j@kW}|݃ nF3&Z'V{j^UגN~>JQ)Bufd #ɻEҌ&1|ٌ&rU:myCI&!8hN~\~%՗&iZRRr Ϝ6m 6lH6Q-;M =h<͸|m_߼E  7c*nX+<_!t6ʶ,;D<=k -E2{n߻mk/t۵?o~n݁mH"b2 ,|NqЕFڰ@Lh`Vpғֻ[~KގǬ֞L/a6sǽ'y-#e*>Mvp~7k`meL"v}Ŗ*`Z۳ nq#;؜ɮ Ӗ _ AnɋjEp,:[Kc=iZHy ]۳kѻV#ནn,eVqv$߼E{N dto '< ~D{ \Xެ[y,!y<7-Vrm$?@?"݁GC}a-0=zeǦ#E={\-TS<X9>MK\"{ O#мRoe&m&nwW]$k|H)%[R/4ͫE֨cKZl`a$&r~};NmfIjC2_. 9OggSBVX6rΝGˑ˗.vM*wC<)}kMjƚK:pr'\6 .gFR*Lrs`s[1Dιe;"J*̡'cD4KhiDY?A  ݜ#hƈ~e )@# 3?A xx<<t:#DV#4aGt 9\ HPmhO8Z#i0:jlt); &W`b^?6S+RA1a.[_YeR%u}ZA<|ZZl97ds 6} HUzI{a@b s-QpYPcb[;5PمC0A-ğ 1i%D**gsB1Tqg;,8Æˈ8O nnM>mm6t笷k/4sSu531vS6xQ*K]05VYgf%:~w tyiÍr~&0Igq#=85m,Ӟ2Kttt%KЂ~ z:t%'GGB^ z:З/GB^ z:t%KЗ/GB^ z"З/DB::t%DB^#/GB^BP4ۏz;{ێ]ҺW{o{XȽD^^^ॄ cE+t^ըw{s{/VޭKE{m헽;{{ۖH"'=wQDBXKovqtBRAww&l1ajcaz[{CЇ+ N xw ;] k%9J)l%mPIh + 莆JP)bYOL'rdk>_?N V tRo*=V8?~3yw_<izߥver?t;O `|5gry,;ۛwKK/9ϣ[>?2MVd&O|gj2ZR`Lc>o;|_ ꮆI&13}86^ea:ƆTpL~Sju˄bt!3'&IoEV.  v$Hgb.O%y<ȍ6%F+ӧ8N\ r]9K(<&I0sfLJLb(L $LVk5c3yq{ؗһ.//Mv:.W!* |L#0*2)yNpG5 szxpOGZ?sP&w'rއD0bg ?\BzF﷿m'Ipo8wr'b{{{f?c?gKxF1p};5KrɃff;m<,d<Y0seGul܇wtm%hzs: ~}+:"4`؏0"%ߒrm*@ǚܦ >_c*" zia٩nߜ9a/Ǯz-Hz>עDh= Vknֽ$JƖ3Nz ̾K%9P֚4 裕v|vpNQX)C%kgsr2;C'XdbAx29 )%wtWKwtCX=ﻻ"ýtl{i2!ʚpMVU<^Du^WBNO{eҼ:R{t^{auȎ<0(zD""-jWJC{www%wŎl ==">_2UƎ n?{ ̴\̎^"4 @ k_Q޲VNk',fV>̙ 8,sp/NXj7N:׽p{F 1egoO.hDWE*uQP%TG)}FߙD c"ukZ:'ϟ">|#Ϛȏ>|ȯ>xO9i {!ܪ*++(bYPU}y^N!]Bƅf Vmo.JIPU"CB^}ҡ2X꺮kE*n[ eq_s[XJ׼s<BY*ںO*@8F+~EQUV6UXU}QUVl4vq|qpmzuG6T8b>kɰV{y>P>/@ЌW_(ET'Ь!V j(H"<0KB=G%z`p<%5Ј>M/u+F_??*x@`{Ceg93N܀QZ4 vU+vNWuR1;fd$ &ߵ` 4ˠSޝ`gg)ER&9 12BEB}Ia H^" IJqԒK@b]8uuC}{Wwh5Eu]DuuJ=Qz܈H`.C'TOgՐ}EUL͕ȅyٳ'o)FKIݺy ݕInFm'o6-L}ЖZ-O."|sE6s3RA7ͅׯV6o߳nkJoݿ"wօAY ߻9{YYrۻ=:}|MGam՜ sY?]IE-fﺔeb`}Dx:tL{EKErpn׽1yBb x(PdygTP^qB T(y(1ͣ[L4f(KPLBx5@K4i# #TA j`EB"sLA1eg f Y1>ŃVV 1f #ȟ#'۲81!H|(UD"] v^ExÇC-WoDѰaYr%\×oe(:%I!JX!qO uAS#Qԑ/hMR,HJVa#epIIuU1'a~CT%-UP9X a]TTPhu@_;Մ8%a%UQe3kZi4Է8I  0*(TPQREj+TVQbE*,TXQbEʋ*.T\QrF *0T`QF *0T`QF *0T`QF */T\QrEʋ*,TXQbE+TVQRE *(TPQ*EDUJf"ܗv"{,./qeZ $VAt.uDmW.w.+ܱzu]z\XK,/KּE\LNW\ %wYdD 'МbFwRʹV;mJŗNA*DN""]zp3Y]m{"({\IVEM""'= E{ UE *&wQP9:@ϹDaQT\PZ kb'dJK:QK(TcTA)jgpl] 'T_lP17*0TFQʈ2΄*)TQr??F^9,t6*!T O3E)7E-uAgBCץC5r1 WfMDI$$I$I$I$N+$o~almD/bXy{W %_ptA 7IGz7;+==3b`bb,]::޺wɭn 0x>f`F⌬oϾo1ѕ.Y·aY'1]==)= ۩ \E g@H@97hWt ZϢZ]h6ђ.!t. QCڔHqMnˉs(,b\i3K^ĺ)\cKjkX\mkiHNP4lH3.LVPKFZ%E(jN2vᖃ+feJȼ u$ڑEE.$'&Bֽ큽iBpDBVEf9r ފDDm=ozH+ :C&j]] %o5jEjUS A @Tʇfp%UUmy . %QUN3TURwywy\DDDDUUKKwy3TE.dzg(HYw8KqUTL  $TwHQ rgwgUU +yy\m=FOggS@BVX8ץ5U-HY]a>˅S/"SCE)mǙS2( ffl9*X @@CWxXuXG8 9̛祁@ȠX+Yh=дn @:Gt} A (Pv0}) 7OJ퐡XT 8Cl@.VnTVXb?#JX;clFey! l핺) .P1. ͸pVlb$H"#1q"@ (@ 9? (&/n 1TBX+rV!PpW4%Č Dp@C5c)!8߿_%K0qay-6$wO6AN>     0000@@@@@@@@@@@@@@000    t'c60>.Wx#E c~E18hfLl!3~a& ,24  ]1OF9 @ <[  P P}$3XLAa7,xw 7H3\ ]3C023U%VJY*UdU%VJY*UdU%VNʬY*UdU%VHdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY *U%VJY*UdU%VJY*UdY;b[hJۛh vVdmLʦz6¦ Ķ66ĶA^h+ XW ŶƱ*W Bm%jUdT򹗙bU/NY*dJ[ȕd\cӭ'uXߊs$9X%sĄ/f@U\\c!$, EA H~e.2Zh\qXc}qO(y.?2.\Ec-,-%EhXbŦU*TQ@EeJzRRJX$I 0B ^(((((((((((((((`tX:,E`tX:,E`tX:,E`J<<<<<<<<<<<<<<4J4 ,xpW2NOK7LU%>S?JYX}RwESqNS)))2S(Re9XS8Se8PEPEPEPEPEPEPePePePePePePePePePePePePePePePePePePePePePePePePePePePePePEPEPEPEPEPEPEP%PS+2("(! T((+h53AAA#MJv"ATP{TmveP%PmeATePu@HP*:.: (0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L*aS T¦0L 2,*aS0 I|*aS"w4g LeeLL*gsT0|"vT¦0 D%J JD%J H$*QRD%J J@$ @$I@$IyUUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbXV*UUbjڶjھu7WruWr\r\wUʾ\r\: pHoNVI$x<HAD&T7Ųr'a9|/)7+-q~Og]G΀8t5?'9>gsž η<ɇ^ozue\tX뜿jE`o=*9 y_/iPN#yl97O Axg{a '|yx{>P;NvC \Y8;H'OM98)kJPç<HMmmmmmfٲzN$.+a 6:@ ro@Pl>@TpA)‘PDd9P:sb C`Js>(P (^^~;}h3KpL|n|^m5K\Y Mb\pWHpQaZoI($(4Ua.꾋jQ]FF貤EJrĢW|7Ni3њ2]̉(A Ww qW`Zq,U Xf\jDf 16(hp2UFhT]1 BWd<`ր C>}>>F 4Fq}@װ>q}x @4o}x}(~#߉1as;~O-\%W=_Upu!|?BnAau1D  \]BvB^$iBYfn1!(\GDc# ʘߤRQiB^gϘ$,#<#18sg9~!lާ_IS{ EaXocH4"xU.SIKD9謩*EeH "E4eY ])` ܰIQ=ٯʀdUza`963ן@f r&`4~)>zCYH{~!p=3E)@.!c1Ya-PD :8I' ;~RpC  Ƹ*X,L1$sOd _+?B&v08[% ""28>"ܝ۞o6%‹̼͸Qp$n =ҋbqOOggSBBVX9(Y|q73/\R@}^YfKܛLa(gSe~)J\ __F rDz35]sq. ֛j=[28_PCcϥ(U +)qnbLoA.c: F)0jP2xhEb׆zL:pʭJVtnw~ 0U7 9tHAzઌ5`{r=βf9-mq]D'y+}M)yZ3񞧴`/wAM~U_'(y}`{y=~/^?0=?LC~gswox)/u}^ψ y8fݵscs=7cy_g<<<'<<<#·)<< ;<ZѝawDD &pR> i#H|=/0d_qW*uTqy!6vZJi+фzXЃPJ BUUnfa3ۯ&Ͼ0ߚaU[pX678Ͱ.`ۊ1gs.>c0grEP*P I@__1vݶ 6wpm{w{﷓:M7]{tߌ9N6kM$ELɱTzut2hæXr؍(*g*qYMX" ECc !MQ[u7=|Òn)9IJG&nXJ3+qbt{q}=ƎǸǸ;vkߋ(h@F֤;o7 \:厸!DB:KWx]v9נkя_ø.G_TVE'S /u<͞O뇉G+/1Pt/+XMB/ܵ?f X U{,X"_঒k ]Gzs4GSX3IAnX!\ +*ßzh\8 `\3"â#@>$#~1<1T@QY4aXa׆c$Ʀ+ɽ)C ͧXfG@KLiG^"`qF7K%r]0z*"^y3 S~0wt8/΅]~bi)Czs\@y`0g< r ?箦X,"ȳO $T+  ǔPO8(?ki#y9ar{9& x|ٳaxp\:\GVAJ2XBx|UzDIMX?͛0PLu(f_QxSo_p]yIFn.vu^+˪QQ߄: kW+r}OQ8 pte/\k)z=eO:o:DCP%}m OK糾޾mcBvKGsyu׼Q'^3yؓ$:=Տ+=pc/V^Yyq_ҦB.]dROr x}Ɣ rE HC2<~(}R oypar2?0e1=Tf{e|/<amp w&6"0dE#dZ]$ #ƛ)q/`NǮcjf!V. CrӂA_ůhd U} 2{?x*-[UE@"W" WDnr`@\xW? r!B1ሃiRIU؃?MɈb ET}”N.H܍ȥ(@3 Z C64cG.t\Tsvr:lE_5lC v]'JOwxQT32x+i>` cK>36[:N{ʢmBAs}?l@0qm̀bb$J\u]lDDd6Cqhuv{71.6p|fbVq u^zx/{ν".nK,Z\mS pJOUky1RgRY Q M.9:S܋K1sq~]wp!JM)>$)I 6߯RooOoOω~gߙ/ݷk]v>>Y>Yv_Yv_eYeYe&p C)yeSj#.%Ae<eHx`'& K#|?a<1~~,mq~x[)̑σHp|O.vp6OONgRָҧ} @)}_o|c-5s p ]ĜGAR'P|sRt0'ǥ8+td+:{I*ϯǂ"d~ӕ2MGŀşQYY|59n'!ҙOggSCBVX:}H\6i0g z{>o +k^̽6 }t㝜?$O3SBS_E_v}1=/r+R /)<<<yw_7Nyyy矋'G=;WQn)/b;u\1SÇN8;s;T򳕼8]@[+lR?"GȞnssy$4HBM[ZDVC=UUUUUUUUUU~UjӱUwRT,u!1v1V U`,`09`EE0 0 4͆((}w `!}!"m;wA0nm}{B_8X%8A PXm5]TŠX0 i.I`Aر65bijAk"PZXa,x5c̬`&]V B]\/^$5qs1TEBSXn/aQ BpWpSr"nT0b.|!p3*.Y"" Zp}=cqGccG4}x=}x`>};Z׸`44v@}4}ikV0C~4h@8'^=Sz/1:ãh:9 0|_=,'ǜ{ 0)J}q\ `_$OQ!:ЬGn~=EKD/nJȂ_>ˣny$nxm_˩~'>f y[j-gh<Y1H(򼮉Gn'|DEi||v &p`FBg<AF٘0^q1ɳ/OǎvH8K iH(_~B2] # C /I!pSG GS~)ɃK0']n/'!{C&@8yB)  $!L esJZ" F9 B~/-.y) i8?5ZkHN;Og?2?رm. v}8ݯ/t|phu G$C.lPL^W(&P!u>8/uPO;`G0.ucٔ1uJ:7`Pt/xe΍+O?'8.5?XG$?%DYsY'.xM}=gV-hqu^`-h/9|Ƈ Ax(}`Rq4˲72(?ȕyA 0]}g|U lN;ϞQ߆:1*.>gIOPOKAGQ>G⫆~yUVWWd> DU~UH>M=5}=D4|D~Ͽ"ukBgY𤅼W~4j Όddx?ߵu${l9)~FXEn>j:r~fd/Ƶ}aךh'>$IdЈQG|`{gȪ+(Ϲ/[e?*eN4*!~취 Bun gV`؆xʭ.¾z [y>r'TJ]ֵճvrD%I+cٱ국'@K>QC$wm)D3lň&.kFKF^rEQQ]ZUI+fб&ed9PT2ӾdneQ5ъ#,lq>l«\)"n7hUe)STT<1.aG\12&2hDTvT]F7qm%+/rp9\CL>TYl* W2:o6" TIuL)i%TX+(f]_Ҁ.f;#;v@  <|\[-NfkSKix24ҚZiMbp퓦8 ucwh,oJ,_FK\6F2D1IJD+Igx:>6V>K rB';'Sy< N K:)'7/G0>'@O(qx*-?/~}c=oy4CAXmg Ĉ#=Tͳpb9A_|õe g/P&O<^ާZ<>;$G? TGA!8pw'k:(p1%;>G}Zvbx 'ÝG %o<<<:<^Czyyy~08>-:3$ j(.xW  Vz=p@W{>a҆4y^yO <姐ZN JBΰНPN{ƓF;y?D9_G"rO b,Co}mommͷmTws$q]Aӕ;5]  @!6XrH@sN#@$; scl}ngP K)A܉{4{׽xyRopHI XaT1&XCXk0űJ+2VA&Fⶮ$ A/hU_u&W7,0V//~$[_u^-Ō^-1H~*px/BX8/W^&&PDN PN=$V"@8 B.X=8aX*.T0F6݈E ED@>Ǹ}{^=ƏqGpxx}x;xƏhhvpxǰ;x>' h`I8DGh/'o' |o_]pm7TpEPS=o|<ǰ_p [v7o ^ۗos :?D u~B0B:OP`2_?Xe c s/e(Á.Zm! Ș o/[x`\]tNYudy8u'HуMif/dlc{( O;0,9ϔ.N _5|Λk\\!{ amA}9Ԝr:8Ïιw]|yGi+:C“wd'BZzOl_&Aq#}?0ߞaDߜ1Zo+n~,NyA+cЇ??<ߏ{ =畲Paeq#x,'ODvgW?_D,dfxߝNRy!ϟ dzt<~FޮB~A s t:Y~-%{&n7>F;N;<_$b_qOl:{ h'\ &ޱӬǘ|Sξub=cn6O :5:XB4 xllkڀtցPPB,lB&EhCr<{6ivyZ 5q1AmNer"y!,<$1$$lMɷ8RG NXzQl4\Z SWp"%ڒ#L"mzRFHI$u]uu!6'k (:S!:#QFALk9 Q[*Z#C@F2> kDZe͗$P.#gLv6YZF%]h}rziB<)mFG4-jRKpTaX[~ZN4Xp( F0{<\]]:\Rrr= yteM0Ktl<Ez:(>5/ō'RN.5]WweD /}_Cc!+ar4ƌ뗸8ܓ]|ps?= pᗾh-#ς%*es.pign/ˀlek<.9''9s<@mH MlpݞS??g?^Scx0y㴽ogS'sNgGNaz <6bMHI4Sa(&4җMe_\}7p8*@Rr|> v83p;KRy>Ty<l} hs~IGi'TwQy43ڽ=W+ھ=v|8%ΧW[(̳yyyyyy'e9yyoFM W<^GWdHρ5^ؑ<^ix)ogyYظe鑒 q'^P| |O8z_^UvV^HLJy%r!w~[f9Ż CdmleG't *8@@AA EXHL%}&> X`%&A@`6ȱPkMLt{޵{׽h{U*'Zʱ*j*C\H"dƉBN1+H<7< qe8ZHDZ#e̔Ib@LKMXW+lWMrq=RXj6QeW^-okb"Bݬŵ_+hMR!6MT&q7M$Mubqpyv..fq,@ @h=cx>;4v(h >{>}xG>px}>}4~>{;^(q</_UB-#-њ p.gì0#.`r1,s|m|V ! 7dudOZ;`X6g;ٷ~n($y>' cH#h[}]?mԣ<`A9 DpD8~9E*'_@yg gd?9dge5? <>OîI`Dgg^`G-z-b5R ߛeDH%J<Òn?xy$CǙ/":̲2?z){+>-Ie7g<ޥ Ȕef%:t.s|y@]ty% _#>x#(Gg6:8|O%AG#$Duy,Q`9*Bc™@m)SJb{T%' }t=ZߗBע']nȻ*VN䡺PG "zKV$\Fk>RNwoGVeeղZz=QкcҡrF$$JKS[ I&W_PQ(b IDVVl{uY%O{zrl1텁_q]gQأGLz:e58G`=5 |O@iTF'^ ҽ*zmcz#F1(0?PJHH}ڹrp״&_ _i[{~I4/:7DH|W}¬E/JMAAoF&BmSܩuR?r+cs*p\ǐ֘kX]T}cJ;P}me"azW־\BUr9(M{M7P9y<<v<&d-NT{1`Yi}][л/t]:V=zBbpXWyp[$mz1@%/ F Gw9cEGrH:O>]q<,|~Ͻ7)"}OP?GJ|#oVǣ`b^^9/Χ\sB</XӋL^/3>@0Ԟ%gAr0৪>N}wwvoP^n>k:|nCO[X pJo7yyyyyosyyA Κ}g0؎rD1Y!T:{}L@> )gO t)4:$"|g<ɾg;E:b` i_QQax=OO8v? ~'$ )0e=lbM}ӻs8Hs!fL- H50%aPM;\ +Aԉ 0 # ! @m:0(@B0RʉWWӌgU kR%8t%" d dԱZT*>h5cMbI$PfD*75Ebb< }Mʩ\ c]*V/vqx++l7WtbOggSFBVX<̼s!"n{rDB0 EB..EDv@}>x}>ϼ}h}x|={>x}>4}xc}>1z}G ~/ vv4(^*s=BW;0#?(vb*\1ϸsxbOp m" BR8Sx@1IYv(ّ'H{G:G99y#Yl/΄yՓ,9vc>Jv'/_]?,>G~.`i+_(,2#<"|AF'1B0_ j8c >ߝy#; ! 'e=~n# %' :t?h\y3 xB`.E]sϘπ1?>$p!D8b%{qYg+^eGRee}܃؈9=Bs|ɞ#bC,PdЌ)eJ>' ?'qyeB'|Ȑl"|I )P?۪f Fyns^|٘~絰6Sr>qDߧ_)y΄͟+#ND.~E=uD۠]Q?ܷ?Ezl&d~b{<*|"P!W%aJ1Ư E6'FBJShe;@/iGyJ *\a yBq `|  m(9  HDr)=;!5)dbc"Dg 0T̍LDAD::QD ^0"5":,FpX,+H\ n(J~J$I ޲Ay$.D9BՍF[#h !СT`uj[U$-Vҗ EDȋG01 QJ3@aL''b7G[\#d(.LT2'K86U ]A5p=8A[D="w^hZz,ҸCeMD3֠̕콍tj:-3 =|h+.B&J.axx#IFʒq+"|>GOYܙԞ{*IޒX[*+pOd__9?/I<^/k=IbU-EfٰG^T?p"qHexzI$̓]>;vxO̽ކ'I!Od*`8|NHx)P'ņ,4P랖S7^b|sG'Dq͢^z# %Ǡao=gP |z N)oR|db#NR_Vٳ~8n`Gq·{b/D7a 4ؿk9zOf׎ ǃwGΟ8j>GE(F{<{<'GgHH Fc}HF;vZ8z@żgڽ{1^ߖ?ρ!gy=s<<<<<>o"s<<׌<o-yH;)EUܢm1x{U?$ Rԡ@O=σoTƕC0ED @* tmmmmmm usͳғ@ %d6+ Hd.lh=G p"3m'yu"Ӂ;s fo\GL R ND䮹'?Wm_/3|¯VޕKyt,YV~{Ŧ*eLU^Q,aPbyVd L T Z&ю5_U_eST5=JP4Z+]Ռ⻫xR)b"jHWMot"CD  ш!*kA`4pƏ`}|;>{};}>hN})> }=>}G>>oEbL~4{h >G9;~> .|t#>tcMׇ1|t"f:Xoe!ueGk Y {?..ȹV@vҺD~g- 'âeA낙hIyS9|n#2s1YH`-,eOv B%A7h"OٞrbD|P<^܏i?񀉔m`S7ȏ %>2X<e~Y~Iq;  s[gu&es:! BzJ!EdwɶϜ32WFy۱=o̮׷0wv4Y!J:ŰXXXn?&GqL)67s12C `x6u > ru Yu3`P-('\ߖp:px%`o|J"F  2/?S\G=5ljۋ_b➺MCBMFW=h:?O]' %_ -@vcSxY\3%0Ai~2xY̡p?3la6Z9<.$"f:Ns`ϕńr|1lP1Рp o OAIĝ|MYX; s%0d>=m}q]oTѻj` z="=׀=  ǀ :/|SIccOggSHBVX=b~ٞp38xr(r0 ^B4n+%yyT6|؏||۳juwO9NP'/0C<YBފ|RS4+8zx>g'g3g `t7qRu '1s:B!x }5=}y:"}xN$T /<^NJhao}pzBJ_KϹ<6#Xo<=I`ro=S<;>/kOx {p}/~gg'b5ϋ|`~^9O9/'/>xxǽ0}{|>}xq}}>}4|?#<~ O4h  xy}=cq#; x4./~va߈#"9?Ht9o98s1J=;CNNsgɃD~3܄.M(sQ+B렦$^[?ΦQ%x'G/tx0G@0\]40A=7?Kiy4qVX}&j 0笴S^B:c?án9z)%J>Eu zJ61\CzSHP.qs<*vGRep]a.Cߩcy 9לz3™&4h}3(J<.V#~qK)Z>cϚARnXjx BϙaΌO'D'=`8IG@3A+\3uރۿ|}` (o$|ˆSxQ|͛7N?D!G$~R9 ]sw r%B>~ 3RuyL TuPQk2b;"AC'h?R Srb}% x j`xNA4"ݬ0ƥ8㇂sh?GÈ^]hM >ƅLut#Wn9QHR=Q 5&QJX -"L1"fR|,BT8zCYπ,4A9fRPmĒrjȺz' )3r.SC+Mmj7Gׯ6턇6D߫<𞏀fTy{FJݍgoSacܔ@C6.2yp۫^)#I:h* 9O<@<|+fm'xO~?S}J'b}$a( iOGs^mdSNZu  q>A:94_gNh}j|X} GAyxfFBMW3!{r Mai~t< ,f/:=/S1JyjzO:{</ʇQfLȏ~M M!txC 8hxW;{UbF<>asrv|da‰OWWKf*c`ytxX*|Mmmom]ݰ}{N 5\t :G\ AB{A@AveGT PR8H Q 88$3xy+&8O+T3ٯq ȧ"'h$.OB5{ڭz]bHHE3+¯Ȥ:4TXEOjTE1e b^+HP*+B "Q4#]W]bԬ7EYUWmW]6M`6EUUUYxR+t@qǸ@O}}x}x|=ƏhP >>>4v=;q>@{Zh}>}x}G>z~/##}ퟁHF`4},p(~(}3QE1?JMF9)yt.cǷij// qѿutݟ*Y`A$2B ~A ٚ/gfYx?ςőc~x^&bDgj'e1u?.JRb_ϳ4[~s r G H"qOo%ȟ 翤F2%Cmz`Oh3Xq|?mߥn\xNfe mX?xYr'#?~c#>DLf~24E`Lq |t kbH_㣝-7gL=[r>:Hc|eߖ]kNkFdmj埩Q W?++@WY'\'~`{2xVѓgG=EbϘ2DV~Q c}O}OggSIBVX>~C ͱ=|^gA䳷z/4QXgwG.y5/1-u3k'D "LMݍȝeLvAh.V =|kh:~:l^*I}zN۩*@'#l$b~u:tR%<( _0yڀ~cUX;8!zx l/Ny0ʿt uS=\Iz":俹KU|rժ0bJmd&ꘊR<_*Q#jKcV"LlmKp2_Zj총nK4TknJ1ZzI.Y_}»褣83(cQr]&ԟ1@1Zgp=3Ȕzm1WFNkhLV)Lr/(QR32*Zt{5&=z@d(舤cFI1~Ix.|:dr%_!uO3=^J dpW^8|2R;vȳDIȋDb |$͏Y<x_lg1=7U~WS4:)Oa0Od~'^8}%8J{4yA|br ,O(<) ?''w5 ^ =xۥ޼/X|4~yo_ȝ?hr|l;0\t}Jr?;q!}q.<DȼaM0)NM_݇|/z#r< Lã+"yEYcʼnDw+y6*}szSt&*ϩ烥8?QWHhsSov$u5zWCi̎^Arv<<<<s. i_q%L$0|> {m{5`{W;}> =l}lsra `{͸o7{|{o}o#cz}6l>.TJl'pC`o+%U먺Q;#op\D.PZ3Ւl,*:m>mm|Xl ٙlgf,|m8?㮼"[6*jyOJu/)$FB.L( Otbn;Uf p,{io?Upy[x±&6lJIäTcj&YRe"(A@TRjhj)P!%QolꚍBYE_eu_USMSeenT*QjkS]dUYuxZ+! @BQh=v:=kx}}q(}>}vWx ]hx! }{T(v4xv֏q>k"ΕU~%ʕ?׺\G*ޯt<^Oa&4Ah4qp@@n4zr}c_sm#"?v=W;?]M/r/;L2Grn!0oF }}hwF?>d#u|fhs04Ƀ~HeK?cǓ/3#&cH̿z\]yGf31$d&~FˊS~f;oΔ|@}AsG9_yNmg2 men|k G0T7a>{7| ?̶ttb?R|Fɸl@[swffFFnD8l݄{$~6DW8k&v=6JyMmsZo!{l谇W9tc~ ]Q*a~|ׅoq~aDv^obz:{i:62ٙ3&< kbɖ 10.IyG&\fF3SƦқmCfg`3ic79~BiVLAliS:=Lo_DSI_/}.>3氲bl[rYLqbi9N5 Ns6ͧ ̎l;Ar xcoHIDl6eųL#y1 ɺA6O#>N>.LYDz̅S> ahyr_5a-JMS/YeOP-Lc %N)`-) F77tn>X,e/1JObfF=aRW`\f2&Hh@E|̇$ܖy:[wݵ~׼ˏs_Z6ĝT[wO3jOOV! ğ[}_P__$\dtJԖܒjЩ>3Nqu{v6}Yܵ׽>s/bhշ %Y0Jή?$=Z}/z-GLInѶĉh1 1%AH DXEjH7 X-vm|C*{UQ/`Lz קx5=JOggSKBVX?);Gi}[scP*cCՂڃO!l7^YμO0yR(#O\ j:-dBIDT`z?V&p5d7"_^|m7IYL,#_d [P)[[Mߦ̧Y^bouPdүO)[_0VyQq%AȠ@Q&ȟ׳=MDHPUI{l .bxIv=9߅aWkZ/Dywmr}{VzIA#WII>W$}ԇ=L=&H%ſݞ5*PZsơU=ZzOٲDV:6Q;VTbUkZDTY^wXA /ϔғ8%^b=X)DNدCX8ՄTg)i'4{9/"A~v6Y%/)>gzUpOFj͠TY 1$%c\s-^S?^F򼾯rIWterQ1 oũ\ }ǽ^nҴW{nVp!Bl(VzmvJ ɖHYepݾWʞB~\$>~' !tk4J@+~I6^?eήCenFV6u/&b-atF]#Oxb^ZdҾF?e=8( 7n#X 2{b5RĞzIJz$Th^RKZPJ'Eu&Ɇ"cHIJ)J1(*964Rb=m=dOI "'+QL$햜<a"^K_ # %H,}GO>N:+D*R֭uUr"RKlXZ: EV&Pkpψ|B[ (vN:.X%OxN88s ?}.D< m=g1;_Pb}^_{ N㵥ǓVfqi/zS-RTY*"" |k|kU׋զZ:&0aTvKxZi2"DDЪ9aƯf%$JP Pm!R+2d4 FPЦC*L**Jq1݆nT+&\6X@Th h}x}>xp=}>x}px }x}xc}}>x}x x?^'CO\4kZ/~ O]W!L?3"iqi~C^C#@u? f}y Yh;8/ׇc+nz&x'Q2+::sHn,ep܉N _F>ǐsO^OQes%p[!I!2@sYGl]G}g k p<'[?)'(&6-!ySO\2q d~^VyL_څ1oN2xtg|`ož? Q :=s;2`\|oam2 Qg`xG8΀t/x0+)9N~)G,ضV3WǓΧc$ kڌ k-h@~֞.lt')2wE}u~_d؎Ä PδvecoAp_ql;wy0rlDutS~EZ5~8#}zfxrtk6gu\a/6՚kQ}"|[S#ʍ4 kgr~⦦Lz?Z=YikF|cP{<4p,^~ "Z|! M=w5/Bl<ϷHet,:0xrڍg>|}&i-խ,? ^}ɳ9+3ZDx/Bbv?XaTf3~tdt8=[mom}|kmA*B&I=7Fih&}+ 2}]8+EpI}|z,}$)T# Y?e.z"$//%A=ϣ.e>ǚ]YDI2":VSXaԇ׷07Q(B>XO9DOR8F1@=O'0-I"B,_>ڊRmF\-t:y!v퇪LD[^d *sX֫;=I?a"1Iu ~7~J z6({!oTO`~3zZ ebmi|ǾTT9Zy<77)z)Pbd#$r 9 -GKήeWKK@$v+LՁD J>HG"I޹KH ?al_BU_ȹ܏ a񃯵|_lו9l#++"L[V1@lWQ35G̸ $^U]X?G-=oAzVDl^W! 3O"r+Ir3wKpԤQ1cQ;& r?*!x) r0p$φ|Lv5,49Dwql)xp|>?x{?'G>_'x玐C؜ǩS|>%~ Ώg;<;*}9evO;kh';dZw#r7:t;O!1IrtA~F0#E?6>4~+y;>N$T;ZWy%:fwVω4'UهƟ^yy瞒<|vyt9y=Z,`4A=pXO|@>U@)٧Y0CF*"E @$z/8{qxWcϼ}}>}4x>} ~ j_3 ~ 4hk@Fvs{#?'k,R/.#|e@p#9tk'Ƶvd8"|FOY؊3 2#O?6<+(=XOONs'"#XsWG39o.?Rhr x"’3(pB.FD4V'@Rٽ9R0q ]y禓azh_ϟcƒ8F&?$4Vs1o#0N\Xcd+!a=Wu>qq#9ЏXZl!E<*G,pWx.G!(3?|@ _!S?ʲs8xu..']1'/\蘦Ÿ?O9(<]~?:::u+, z(&5HG;O1^~8r s }A@R ^<¾bd71>L +,994 Օf.(ѧSF2bdf/'tN!)n#=rf =32=i.]XSNx?)Y:aܑy9.#bWR;"W• bA&G=9G@i#ɇ2(` (G\F .4莼ԯG?R1GꢒobQxI RBJ<)qc^z ] =p@=ۋQv(qpF)qf LwL >uzŶ*)%XH!X6بS2=gW(m vݿ54OA~'$ȤuI~ZֵT,cRVyr7#rVxzY0LNDk8X}' $ol6VؙFH\J~G`6D 9%6Π]^B4"BՠDbXLQBȟQT=T fkVӰD,{"':޲Y  $@F"im*bb5nEը`| 䬰9F&"T#wlFh{ 8Cb{Xsga A6W6^rą{PKV`I'yZ>H}kr!5#TJK Q)Y:1%\9c)Б6 +GC<J^zb`C~3J.,.~pxGzgeI­@z</q:WSާ?As;.s|g_j~;S؝ߠ0L>GG'wѤ90t_ZlVSOzzM}ov;x`>{>xq>P>xc=>}}>}x }>{>W-ր4h ѭWC+uQ,& pq;-غo|NA~Sn@dڧ3Uâ޺iF|ܜ)^s ~ЋuN%$ʏ=L#D{G(0G昣6qʉ=3!磡uB`VR,CP)??;Dc py?WE>}O8a{?Wa‡Q~u*X{u~L J8ԣ⢃<%{ )}?"WF "< RG?(p " k *\fH]i311hVп{/=r²"! ` DaqBb`=)lX*cG/tG"Q|1 gi ^xul!y@Ğ XY 3Z,iא]u"|tp Ah)s#|+S@J!~#( aۈ]y]D•/sQ><`w3Ywc.8a|3?^WUdu(>l?8uX H??s'(Zd4 Gg(*if9<(Ž1/Դ#qTгSys 4G17$#QB#G0D),:X-^A}oܗm䐫G[Y?DĪڱdD#F0bsA=3NQ5:)X丹nFNFN<*]v/6Ui"$OYٛVa9G6Iitr7-Fm# /=gK =1bMm]7vA$6EOA&qURqlT{H|H@ңV0D%O\3ѤJDP{bG'Q]1AB:GC=cQ9+#GQ*FZߋ`B=ѯeRsXa@H[/J[ k2jD.OggSNBVXA,0u!j <dV12-1ұHXV IbDV$zHu9eYBD(-η+lK9Ǵ%Qŕ94 .+4t=r'?^|_ϿEG>(}J.Zw}7JcΣD)+9Yĕ +]w< ͮ|Y4d LWsZ{x,ǒ6xO_kx~z? J߄DaOF'F'gSnx=b;xpyC0s>a减$~WOUh>ʧ4&|XTmGx.99a욁k1t9P}>@>{]L:ӫgr}*M\OoB'NX|w9y tsOHC>.+>/<8<=_^yypyNy珐>t[vs,7ҔPQx+y[z4/d?g@4ײ,´,ĩG@Ky z>xx>}x}kvv>ߓ'RCq?4v;=/ |kOc,Sɏ0GeϷ- -\5Lcl󉃠Y9̭.LfnXArw(qG3㿜 _sFVeOq(FG_/;K?Y~r&fbx cn?b<#)>{pȭ)lĐ?9~/_&XOG 8 Y!A.s4F3(d#mm&}nvS/99⹞pVH`EL]t.͉1à4SKR/E(c~g^\GD$VdH+V^Y]f2pI ,/^>+6t6Z=0ė.=pw9[g3rnQrRrrSrSs߹.NJerrR"tܳ[-k${%.rp{<#$lҞ/ b_'7➑#˔k,BK| *q<'g z1N'=Og=9|~GOF H y0S|SÂ|g7dH -볂fS/Ϋ6t@||^j aȜw+e0_=r/럹*0r~,7 EC > ^ays|OW 4~/L' +ճ{~g܋a09祜xO>x}x>}>}>x>x xx%րm`A +t}~4 3 0c=iiWK۳6^MF [;re,|؏<bs+g8MiguЎ|"g1 OAy= c1s7oE?5e>o&O.chP!)<2G<1Ј-Ny||1q s}^3JQ(oڱ,Qۊf11ρA&?m')`d`GDz&$]u8=#" ]=<|xŶIJ@g;cs}V6Ċ̲vbe'LL."wmRtǕ$0@` kN R^AT_B'GW_#畁qAu׿~/Hm0'yR& r:p^~"9wG"8y`J~b.xݡ=r71rYlymWAF?[uyQߣM&"ߖANx(|'-OggSPBVXBj$F~|OŹgx9,HN?xϓhy3y矏(GmLg-lL#< V \/bVu`F;g?_/кbR8x+ËwlRSЦ]~?ЇyjG1ÞDQ<TЋtSȇyJlO'qD[cGBc?s'~ Aq@&,QXWqp?NB9)x} D-LHEes}3@RDv:lvq%MN';T;n!k' ?k㧛cKK3Au*} tX`ws..SSD"mQR!Ja4m`$rRH. Bg#H0%t)zOd79{`A'91c| cgc.n!a3PzGn(]gc֎ o/d%bdۥ=ߥ DlAQ 5)%]=rʄJ$鈛A__m [ՉOB833L'Ia$Q-SZz&;/rO d g,<_#!z<(0jt8Ǝ$}(O͡QjY?؊~E&ek[!0EOm[zO7rS <c$rZ{.$l \pAILWJ*xyHw="KY"̗mkҭ,%?ds8οG:~g Nɡn-|h|Ny}&3u[;|o&yMa1`|`YPJr b4NllXZfH (su4 ` !e ZM RTb+>R6GR)O2HS(I#x)2IP<ZR`P(U%DAXTSaQ2"0*/۫nTMJbT7SUUeVUUjy+BsUQYE*,XHBjxj{ e@T,_x>Ǹ={;x4}>>h=>}xƀt4h}}>}>ϼ}9?wGƃZ+7 ~~/9ߍ$u} 3ɍ؝:?Ջl^0r O^? g JfYOYD v|x^g`_ˁ?ɳ,lgF'[RAH&ncm/J ~ 1Os0{&|OQ 嘋d4~?Ycle@f!ed[f,NQyoY09l<}Ao8sO[.~GGye#̋vh?A΅@{b#1/?_͠SL:E|41#:/̱YB'6!A&Z|2(ǼN76(Dx6hhcxx9/ ȝ|%? >eg$<_FȀPđA*+mȰ?~dO [2,{XZ`G<9(o2blIAr#(+HId5,M0@$ ȩf %B<>63=nxkS_e5&|˕9Vk4Fv/v5z=aXѰ\O],lI%9GG3&Qj|ٮyꦇӀs[&?s[G?C{:}Ngw<<<7=q<%U9N1<RtC⇛=C c xVKއ gc=oON'S韐~x z ibbNSJz:{ި62P.2m mq A%($dZd C 1L͠Đ&9<1璨*6mL$ 2 5;'Жd5wPu&*WY?Ȭc|k VTRt$bT*Jŏ h=jK% ƭ­ZLP İAX+Um2Ԕ\ƣ`(<(7= VUefbگ+j)nW=as貋u[uqLSe7JR\UC誊誋1!xV+5AĄ H 4b B`)x>=>x}kx>>=hO;xx}}xkxBQ@vZ k_[$~~;']nӯ H`1ЅOpX-X_?OggSQBVXCo[N^.s0MK&'{?85!Y?o g/Tc>GiL ^W,"As^r =J~%3C Фvgy%ÀZ̀~?"rl6W0 W/>_?$]?g󞼊2_<~si7/~ga e7|(X._1|c] A&Jf ϵo[`29GF #'sN ?>aA,dN>#_#9&Ji~G1וcft./=twO~B/>>Hb~X# M44[B2#[AdPȎD'?9b=Ppfu+ 2 YyS96God!nEW1?b>! %ɿ;0Fs&XLCoT?0ǖ"b9`){_K\s 2,BHfg ënm)oz;uY]abYa!`XglpyO])W1|G.5l P[8rxH,a2%3+fllߟq?-|7W(D}Ng_Oz,ϤzVrct/GgȒ~ZqšJ7HN{/O[ boVw9 ZCN;|h<Ԕ^Q{_Z|v2FO NNZ0|)yyY<B0ԡ'/>sd':jg<窖{tUVaGx@9g 3LϡyQ k>8<<4 +Pyyy礼}O쓡NAn;=K`)@<^vj/t>G&x;;<r*χ!;r擮0y%)<$v0)C`s7ꎬ)Ȑd;WZg>d5NYTͤևN0< L$Ct Q6  nl VbA1E.dPݼ A{͈p}x<8{M&B*lҙ[d3VJUD@Q)'"0Mi֩HBT.7 []R"$0F:,Jjf&CdB 5EȸXWF&Ջb,kUAh-5TUvEj]MsjW,BF*0]eX,U1]u*wNiT 7.3QUWXlv+ V"E5SU&!j+a`P, }>xv>qx>}{c>q};k}>@h۰Cx@}(}|~o~x_C e"T-TnF$x a+?3RGD. qg@y~B|ue?h 8Qqlo1_(?o /+eK!~?jE2y][yBJ e?.2?cPϢߘ gr>xtwRs.3gOmQsCSq@GT8p ^yye>&"2/~x?!J6C8OYB~p_1gFSA~$3"OPmEW6' 6ݛ2˸{]rVZm~ m>Psqf2-2_砷Qξʌ̹19!je9IR!qCʘy|/0Fxt 7Ӌ;,,`[A?6DYR;rl7  P`1(G8^Gcç駬e`GNY_Ϊ`]O5=b$F9-o#=bJ~n.E?~YLǙ' B?R$!puts98q9XLR|ཇ,ҍEuh|s2 <)BD5!1Έb9C2} q<l弲>7?A?r~moK1̲Ly yodN;x=|'_k~Nϡω)D:8>g/:'K!53?Es+>XSՂGrOhߩ a{%owtN|{lIwwH)s(YbPd[A@I*L$>S4@Ye TVzOyxx,<|@ s>lqϔ:M0DQi ",Vc"+%AkIM>bK_;%8Yɖv " a4]Mj%)K(B s_Y-VbI)3C@ LQEȺ4f\]V bY5*"B\ ]/[u8,K`m͚1FJY-VnRiӉX`Ft5ѲV+0EHN.)Eapc=b0EUPG@h1U.5 x}xߌih}x_3cX~ ?`?W,;q~/;7*}7r{bb:3?x}NzC3|΂E3D~G>-? KY3eGe)V7 o&+~W?㿸=Fp>[o\S&Qk/A=dǃ̒~S`0t GY%ν/">9 u' #uC<Ǔz@ϰB=to)zA[ 0^ؐ$t`8 ɞ|8bc:_MAS=DI2#14ĮZm}O-/F8`=̮.pS<Dy11'|pA\BsKc8x^CK,] Ef;_`\E1\Gi4|;Z׼ǓY`16DxθB0#?1sm Y>au0~AkyyPg>b1Crݭ${嵌 `7SכO=ـD&ݺȹWn (O"/ώsͬ~p#9g<;}іO/!̏?~?_d9͝m3 AICV~.so"8gH0#c<|_<7#ܞ!o˂>^r?H1ٛ:' ]1ߟ?77= E,#ХtuZabZ q~ _PIMS*:gR.شc08B僯#Q"]dB~B8O#O? A?/aGRF.ϬB8飮?o?GM c~ ~:r_8cq ˇG~wO}֭1';]2C 0j&@}E2M*|g$v$ f\le.J9^84|Uҟ*fq':Φt$x0]xD>p2Z(=4G뒑lIϣxAN0H+&a$,e}' .Q)B:sQٌjP">m"zQǎv: y Dqǀp0G@hi(譜%ڝ^+j0q%IDƁ(`ٲ~Z4h+E+hY\`Ƈ\TQ$D aEp_>gWh3#pO|?侗+>N8IτqNy.K5:IP"+BztAdk([Z%#! 3`vrάtw{*]OȅGxc8+`T|,IzO'U>;kG#tA;.vz kD`٫R(,L` XXO9F3C'80T|) ɁҹO#7!q 2f(Y2X``9f 5!>K4˭.:FRuRZ3=+d՘an0Uٳ>K˫`Yi²s+.w \L{)_'uÛ%#\YGe=i=4.zg:6)PC|.[崅|,)etpیg-'q $vX{$BfI #]m@ "V|_? xؕOĎg=_/x+*;y| ?/UuHEtçk{>U&3<COٜ-0mossJs=:9{<0gBSF5ѥ ev =*^Co|y$Qy' ݿW"Nr&ONyyyyyy7{WWסs,:AG(CR#`@{M=^%)p|Nء0By據*gt=1Y«#CC>${ Z'L9o+`tB Ыx 0@tEfr= S*mm}}m}m}M rwpt P"qDEj Hȇ8B ozB(($G6 ]_A\/8M%ij$DPj5EML "[mmOggSTBVXE#-dqQW:0}k\ReIx0C1ETPGюPb"*B*rT_nX#fU^ nb#b8*pxM͊.^-N(WRdVX\dB1]v*M B,qv>x>x} xpx}x4~S}/j{$1_G$_S_ 𲟀8qs:~s{_v򆽡ΑӋ4v:0b~ٙ~`#Xƴ /b,9Pm̲{OQxȕ`/NQ) 0#hu/H]rrw 7ᙉ̳3,G>yK?gu\_Syo?5/?,!u3XL(?~b,8_輾E_eGc'/2,Be߁@E@-6򎿟=j"a~82x8cȤf"{\ơ2cuV /h~ce/_9ogϮS @'q\FNp9Z/tdF#nr=]s"sZ[1, u< ..!A_98Dʖv%?ɾ,"GC='r6U|dvCgmOldbw"_qO]um{d&̿3,-Ns<^hc|Fru`a; dmHb @?J?Qdpo2~?ʡBf8`1"$:a?F:[בřt>N|7HFWdzuD؉H^]Ƣ?7݈F < b0y&=csjA!om+SC?Za|t^!诞'I[Izu.{6*D;$0=DSGy?h0s@裏[B^bCNaNnJJD+NO?71Sy~uH}Jת= S]&RiB~ `E[;+]!#^-' \a)]밈 NF<,t[HTNKb RWlEIhLZT$g) $Q"' RzIJ8 ?iUڰ%Ez:PBPؔ-I􉞩yt<)HXVC,&,R%IXUBjY>_5}}g%I8sIy|r\ZT\{iDr-J%[b( Dm}rEA?:O7dIad ,\JDz-d~dsl 5}80HgU"W?}HyU{׀"E.B|1(؊\t<'jNEur孿7o܌/Hǩ7ܥ[6`prg<wHϥy~^.WxtJA]aZp)+R\4\%QL{Ү6{KpkY#G-NR @FG[K8Ӥ^OO(b%3[*rnkg.[<' ~OgΟ#ȟQ*NU<:^l9wukgWD@E@PA*NLqNB  ^G%Zurx^2n QãC``_YB̞vNE1l @( @ mBR@(-TCecAׂQH DA.Z)H!T*6ieB6BVgI l*Qu!RoXFmzJ.I TW><8UosJ9I>}UfXV1_x)TR\9HXT*V ՌM%H&`$T0Q}T5*£UPhj4UQmV\SUwMSu6MJU+j*U|uMc'\Mt]DUE#V/St$ .-Bpk.]C]Eq( }>}4G>x}x>x{_ߦ}G>xx>x}xޠ5G#  {Z=߁+B~!}? ǹb-eO&XE1?Lm"[x60AA9 #O1fGEuV@tLr|̱+\_“|qn^V2>l^1]_nye<3Y38e3G>|@1cw6,,,>R/>$|^H`P\g;m獯y8fwF| $ˣ~ϯт w}u6.yS"?&3G$ H2xN u:|YHmDqF>K7 ?8mYdu?gjmv ;yy<^F>Bt ">YȞDsyx qߏ΃c5dnJǰCtlI1a/H ]D9ͣ<_ Æh0{~p~[2͑j~<?'+S~` \?$5722"rx?/~Dm~~Fc\0/򡐅Xu%}#E:&;!`]=a~uXN?:]<]?B:낇'~7"9i?h9̮qB9a#<5 |}3?$~Ie =4jG80S OggSUBVXF7(. [[qu7;j}',)Id,LEJN; dZdɑl'7H.寎zs8b+WWSV RJY%ekX!mGMAuO%uF*GO^_O[u0!a])+~zOwX!Ra)|r5*+{l_&6br_H虜`xgSg#$j;OJ ΣRigD@~QĬQz,E p&ҪϮVOYB+"Cլ)OTB"^JV&eبw#h ݾhV3 JWҨ)%FUO:Qo^M#'$_juZ6҇H>7I\Tǵ$iabN1#3Q(]R8Y`‡IR%˗ZǣZYdGؾy N($t}`{?ؓ_ɆvZ/r'.y$PHB- ˄zr;A$]r'%w9}{W\=O/"b蕼$}5`ϐyrbqk%&t*)LQbp\G˥tp$cETdBAP\9x*9F Yr(3@4f*, J>g%@>[&3_' ~'>oG|y>sصpBc! KOGq0^ϣڳ>f!^@z=4-)I?A8_Hjkg~9~/:=S`sN={'&{:>׻!9?D8+\?p`}[ !D;>GK|'98PC^h^j_N5|iyLΞNhy'O9G' =tս%9Lyyyyyayyt (}ey"oa{0zOH|!P:w^ 42jeUr".ND= vpq8\ NYWqȊHi/>~,OhC@Itx<{GY^8MA[(9 Ga@q` V1^ HPmnvۻwwwwnݷmnv۷wvۻmmݶmۻmD_ >|S}} ʀD'_#hs;Iڧހzj@{[r QI};Ӿe.#g6F<8}R8w7wyͽfm71wx;6EnﲉxUSϔ]mK%|JˊJq |_oDq{m/{ߗ߿~pM՛dRf] HUq\L*&dW,8qViN$Q讏X1`/uaWUoXMt!$ `ŔSRX,K;1ɅcTV4DBq`FªCL&&]SR&* F..(0UElWU5St0=ʡPT`7UG4]QuQUUYUvۮL69|CTj75MuE]TXnDa8R M &E%E!`J¨x(1s5ن!TEW"ʬa,(P*0Ѡh`@ րhxƎ}4t=Q>>wxh;xxpZ>>xx>}>=~_}u,G/| +[Mg}1{4ѣ@h;4`cAZÕK~2\SG/38Mc<$3N&)|(U%-f[Үi[y1vΆ **-!WmWx+Xp]l@&J5u۲zJlҮҺ5lvm+ݫJg+>[umOTvp$v+]Ov+UӔ^!B.xO\˶˳u_0mˮ%9P$W<5v)ًIh洕#椯ve&{ڐmKG~T3>j5͜*| Ei9%f.bbW;lָl3.I`dV|l Y"lW/h9}̕W\Xa oum\3ʓU^ZO[hBe2hOm(NU@ˠevZs=+2/Ml\v[5;Ieғ\uU]W+.koʵIB&Syp +xDƕ[.x oF$1o4  x]`m:akI{zm32z.m[ԅS-~gwZŵx$M;׬~]L˪Lp.Ng*ݔ͒)o{Zڦ2mvԹ7 \;!Rx+M~`$RKe6z铭[2d`)i%:>r3RPJufvY_-uK.}\+v>뮹˥4N9.IYY\-[V%e _]kW wk;:Wr7Lvw.nȝGj]nb&`5ĨuIHW6_3\0Aς{cVXXu1R6o՟YOw +33e{+UM(+ӥ"ڀԸ3NLA\RQ\W=QH"p%\t2K[iԮBavHdrdxTJO9r';F#'mS>K;}J0G\ς:l}%F'w*s'o1|(c2}eWjsfѶͦ,l] l?RFdGGch1feK`c}Ondx|ȝbC6iK&}=̿ 'eO⿈5xc}󔿺HsgW#n 93N?:-almd|5<>Awi֯Xo$Y72vw L|ۄ9 o1?|Immy<4E'<<2\?'o|j~ȍ1"r\J Q9hHAX쩓 Ht^oB$Qw;i2i/B<8)g8աSe :ʼn-4ūGSŴG:TPEtHU*Gb͈kM1jM *@RS0 K>@E 8H X掌V1Q1fF68b1ERKz4'iۤV7rOQxb5FF[[YYdOggSVBVXGrq,MXѨfU23$Jd(%%{Rs4 3  ]}VrQ,̡(s) Rn$S*2՘$ޑm)?In}í},ަyXrZ2)yDLs]cxmĵ/S&QTES4wKw؞`׺[[Q l4Ɵi!AWNJ`ELۉfiQC1s,(?HVZ +q3B.:"΅)BsPjjR) Ϭ'1ZdS1qaTDB?8by;fmU*T[ SK׻7ᆁ"[u׭OH($9zDN1 ġ:Q!A3 RӍ4D02՟"mˈlkee@h"R"T< .;AJih3fF.ȄG~iF$obQybc2y $hbGJ I]2lv/E!;E"ΎS Jc[E\Cq!-$`Uzu,ENR}۶.\I۶_bLFr}5*X,l^-`lI 0b̋[#O 'D6FE;m =#t## Bi& ?NU<Hjc0!R1IDGQ ʇ.pẩ[Mk@^ 4ɯRcϙIh'@H4TqF3J5_> ~"2QiU#*k0M*9 N/Jbu]!-0X@ HGYrï!JG|גKht"g FvTfs%`j[S-]uPh )uS/P׀H zId,H%-OZA>r\qCeB4V8GPdPj%J =i9)Txthԁ4<8ኬUR+ 6{`:EEL ,}kd29 )<]'V=!KF⠓MzK=Z!E$' S #?}c%d4ؕ_ 9j'_"I?ϥHJ z=IHZ2$YT=EUNK(imcJQq< z'9>'~?'c&YϏP ¡/ʩ tX$yz<dF!0cNo\z{pVio=OOY?GÓ'#O^Ϡ02{J {D(SrSj}p@G- *;jgG8E ` 􍉫`Fr(I"emBzYJLͪW(u=RbzĪTj8 VTHL(5*¡b.LcګT({(TqVq*iR4drILPj5+s.hyffBd" 55FT\EL¡0Pbڭjɹ\.75ƨhnn)ꛕ t]qꮼ_mUwWJq6(1F4CƱp΁jcH)V+nR.+s HxUV1 +,c up MR5YH"0(EvB4{x>zT4}c=|cxx}G>>( xxƏ}}>x}>5xw|'>D{?w=;vݭkZ Gn=_o~/-f`hs|mi7U8vͳ?m݂DO&|yJ~YƒexNgl)2Kok/;* 59e A ~?- =<灠Dm1΋s[+g!s&_a7wyܡD?%S~qzܘ 2wb lиƬ?tgjYy1/maa|I'sK&'u[?Du7~z"}`ߩff[23qϭ ND fq*A2[D/'/CuмENsg?moͺmʈA>[80OI<%DD|Kԇ6sٙ3B%>ۑ&2Ot1ԓ7qx6_y=g-_/*p|u ]xpi0?>\;.!KgGS:%xI<8eֲ[gS?p7:)Q F %/C(Y'8O'$9<~ssvZ -io'9̧">u:#@e3>`tfeS?s/B~O^ N!.+9Ϝ u?/݈2y_Ȕ~cGI"&'_}R22{| ˞~@ϴASG8ڐkOE~oH[:iZ|O ,F}vgeOggSWBVXH'^:&#-pKc󿏷2 _o8e?pso[ Hn~B; t x?\߇@)Aˇ?]\7Pm| /"Nʤ,ru'Q3jA^ev,%a=](64+@^Lb*s4 yl:qs&>V\0.]C O@KMe!} &$-E)G$qN-,dp>_%8RX dLY? yo,K aK״1_m('ehc4gFE3jV@Vm~y{얟kEzO z}ۡ]{/II!rޢ^DESޞzD̿x{> G5ifNu-<=#LS՟JwV[*u3Z@#xt:&Y!L@xT$y^a72EGFN b֐\ Q<$iI@F2;L, ㆐\pOR J<#$)%V JXEә8`\/e"Htd˴1ˍe:2tZR cQK= %dradaCrT~(#dϾ]t}y|B\Yy=%8Ye> Mڕ2}S_Q;֓nԨCI%)EFTnRLv5MhhTIJFU?dbcg(R\qmӳ/ ܸKDk}>O|%3p;eޞZN-mTg*q*c—2蝬 $BSXًb6ހm RFbdEZ$*JsJI +XN)8#_5:1)"j\qxИєc0[#ҍ !jŧNX4b,CQK*&TeDJ嘙w(a@fE"$,^BR}b''Ng5%)௣_{Iy\OG;[؞qb. '(g 7_=x}y瞒0<<<<<|x{;}k- M=20Œ&)"_ q>gv3(+y@{W{\%SA Q5ӐItxM^" (8M UStH (py..l{mNp3p}|fjFtP@{窻 DA trJpGzNpp1 7*Yr0P*+5%Ԕ$$QQٺWM9zⶥ%D" Al71☴dDEF.k"+`n&lVt]U՛MqmMsܮ4EE]f*ż榪nxĀkblŸŌ^-~b)N-7HD=ۆ8@b^vp"B]sq+b@⋷ 1b8bU`B1QvQuXCQf P{}cxv>x}}>}x}( x xƊ@>kG;ٯ>x>ZׯG_cW_(v hѭhր=ƀ׋p~~/Q s~/qs{GAtumq.0/΄.o.t67_L1$efM獼kH<g=u/0KiLrş0?5θ79?87R͟_/g??/ud3׬п,%9J?]~߱OZ?fP'a"w?c8 z~ gc|y}L8!~ggecr~fɇv77Όcr-ol 9s3??vuA~:Mcd#lgs1~c_DaѴdEN#i_E ]pъ8#o|r1Ѥp[Bמ_d\˘/~D.F:a=)܌c>0 "20a?:qc7=xͥu0J2>ǹ/ߖx]$Ʋ̘w at?;fny>!'#. y[.ut"p2y|i]Οs7'ykNOD` OɃqW}3G>~;c'v?27y` gR3d΅%6h.uͩn8vy4t0v;<LERQ y8ADA"bXx> !XAXp_/OA0V]ttuΰ@(AN~A6lE t [#qD:cs!߸_" NX:a *Hc$3 6HEJT&f&JR<'T$' ҴXH%Pji8yh,dAf% $N2<OggSXBVXIGVR#QhȕMt2B,'Td-b3&.2bOQLU9"J G Tx`IKy# 8{ԕ¦J6 JS+٥nRsL\Ҋ@n7<WB̚A#;4,zG k(s&F/<"Y٨0׺Ϟ>kcל}\>>7>e?3Ο.C}w= =.6y|!ȡ8SpWY4CJzCyy \<gĮ3flQf]~kscE-Ōc7p+]+ 7sjbsH,7 1A 0͌c1AC71>#4d!Vh=Y*f4bb5 ]E;`k}>;{M>>@{Q=>}h}/:ƀ4}>xph;xhxt>Ǹ{+_C? ?W.Z߃L'Z@h@hѭvh}.~R܅>L;]}Ki}3n2_?͖3N_#BD}.k ȇl=Ŗmdٴ~g IC8#!sYi/ܗ@/_ozF{z7H<~gxu8~wߖ? DGZiJy1G;@{I!Cݍ}\ B~d' ',@t`11߄%K#Q~^mux$+u䮺\ -/37! ~.|c|SQafWPJYul'̧(0N9kH&'(/[d"m exAQ#Fmzh}rW<]9gADmB'+C?6oGѧ1"bfA8,f.C(PpBTH ֐83N> a??F^ ,r`{,Yɞe|ΦDDx``=1b$9 .6yoOufO{ 96NG#;.47di 2:{(DK,n_8מmxN'cgd4 % .󫰽.gV"~>soa\?y i; =ug]GyC̲w,OS_t(=2u$) ;,7^V}q(߭_ L/XgZ?2-$bdBO(?#|s|ljt1P{fΔkA$T K_2.T1k/p" H(΍ ;DW"1Mʃ:uHq PxѩKϙ"^pd z^J~4JSAc=wM<;{M}ŃĚU"\ )'h=M4dAzeJ$u+Ԯi6wbdv"udh%M,]ڣfزup%:4^EE--""|\zQ0T"QBY ;x,*8P-ܑ.n,VQ z-ՙu:E) $RGQ%6"/FCF ؆,fcEʵ xtW쑯NZl/!ג>%X@ LIi(WcwWt|0\Yr,JSYYf_apW Z%ɾI)CziI$+Z<>\=*1ExdrpF"*Q$&gd2+̇5KJB"'R_UCexuzMyO*EdʐU䌮Z%HBFiL@:_ת T+LLĔq(MS( .LxRy6?aX)1XeTP`D~ ;,+*)+xpC8 WL)G,R/6C֧+w@p)%\c n^!ڝ0" &d*\f$Ӭ]/ ӎ^yljBgv}O4"x!<>= У[?ӄgGx(J43lz4|6Ϡ D/<^BJvnySǞMt|Փw؉Sx=ޏ[s=ϩO/r⨔@Ob~FgyLyCx7=󞷞yywO3я$bmҪ;Ei0fI <@''Ӥq'r ,\:"?Nay|39ppF6"`>zAz19CTDρFAuv$HQ!9H- ,PA(}ww}wK?U( ceP3yU `Ad flw{RA( ԶsV 4Cޟ h<%l x>}wG}x}qƏ{_xz}x>>px}@}"}Oq,h*=?OhZ@;ƃZ۴|?'.0*,G qW!O?5pκ, ˟h%?>Sqm7'އc yB؍>D mdp'}m7}Mo!HYYSl(atmȜyXe'5F<|f/?ܵL2;*A"hIE6O>lY}z~RWӴ4e&:o#l/\S8A,~l? ?0֏9{%tو'S|F9W'%~XߜڱdO؞nZ?- 'oP>c̹֓D#}s?B&>b[\[HԒ16 %B#Z݆fd tD ELte_@r_`3MCQ W]u|3k!Gϙ`ݎB'#h{ ȲΧSݿRDN#39!YLKJcͱ:"%C{6#? 1epe}\em>?ˆ8/`,cloy>ۖ?Xwd9>'54}|LDG1~`7Ým\6> !V dA8βbvMw93bP{i2䭢>F^=?"3ǝa}湘;P}c5xɍoτaι,$A"#;2̭9 (ScvnW,mZSX_ Mm拯`:Aԉ9&f`7Ap@qHSԂ]AOGUㄿT[8)]ۃ('!2?D` 2[9Nܪc˪%,wE"Uܬ}biԻ I+ɳ RSsS)3>g:Bgs(2YڎNGIG7^\^hNC:ϑd;.κ:9dL||)}r1=4zw./rW8kMw"@w\Ti8߹ϼ$x?yїyxVr53O9^)OY~ Q29w{Y,3ෙLQ@Cߟ:UJTYy0eNr&3Ju?OAZy³K>𔒌k%pSKHjDfk#Ti-I.[ɣ.#eήd0;92 Z&Aͻ;mHO JvIfŠ'2x,z&hބ=)/vÓ)JDw}vٖjWN˶<'%w,O\Y, n+:4\MD?~I$>o}ku0c+")]* ɊIMs"IB]ˡ$N=VW5`|[9-0Ō^J$Xi!mKS+P%)ޫң$K7X+S:裏^0H| #xXM +iHʬԮF8RsY2szJKF5ԸVXOF LF2IM2e8^1;[FX38Ŏ188o '`IQ%Йrǎ_Ob} >%%LM >i)Fg5O;=9g6|H$?6OI'#ntM<^{wyA5 (sbZKG\Ƙëo $EtRpyx2n|>ޟo DWǗJޝקvsҹGvyju513EKǫ{^}PG}t'YNJs~Kx)S:t="yNt:Ny;gA<{{߮B >gwE>"W=N17Y(X'~w OggS[BVXKvvSJe)>TNg)xꓔ)0oĞM4VPA`p6 #39KŧjUUUUWwwuUUUuUUU]UUU]UWU]``710%Vkf caUZ 3`a~6 l϶c"x>W0}Juݘ_%ˈ0.ﯨwd`^07lla{mE-lf000cc@,a3m `ao=!ߛ|߷`1,gF͢YJDP.#ƼQjxEuX¢*6 ::\LRԙ42 Q75WblG &!75,P]~ q^*q=Ӊ`b8"ꮫX Y)5=ωH\nk]F+*]^*SwN'IJ,Sf+]!^.ccǐKF1(EQV*BUb ث3 U@m6 h}>}>zxh};/q>4;B`"xc@}>x}xp>x>>>x}>iϼ B~o~d6  vj hh`t}c$? 7W_ >:c渿]?_5km D# Wǟ vy]?|?9$id۵,w8gqF3e瘉#to!?)+1> Q٭6'| aX#xuaJ •~}㓼{3Q?~GFq~wIJ-09̉η]FL 1xr?s̶&='y΋헉I?ݧ2=n^1xـY=(YEi놦^m#g,#y u϶9 !8-( 3Ug6 ʨmB::/e.NA~?GǦ''r:te7coя- Ģ5c|p!{Ktq N  G'~4#8?3к;A|DFu@Gp_ i![n.-/L]H]N;CMG߬(YW' _)?QnPHH3}^~@9D%\8Xc`mpJ(S]w::?2YtўwsBڳFW}aFK9:tk\_~T~|5ʨF}(ˆuWH ٷE6`tsβFRDbM*ADCϪ|LUGRDNL #tY'ż#\kÂRbo¿Eegs5{D-SIsXSVFZIc%L'Vk'>F8IA1'feLڴģhl+zZ۱c#1(zc3[Y2>rvLi`G|dp^0~?XY_8zZ/#g|'_[f&<4u\ |a'.dSs_~afNkƊQ=߹31y9#-R#UYRԽNDTq.!dU-$Ep3S=ˣWî 2Jcm9F\uvg|gLM$) *yY+ψD 9ǜYCPVV}fV.~`!PgԎg:}yq:ΰI:VTPMё%dw4VD&[Y9,! O'k} kx eYq+y#|S$YaO )Js;a4{ԕl$Wͨԙ)d$\œF8*'afС]¼%"*XҤd8TfFa8)Ą1Gnc/ e3X3ĕ8)S߸]J,1fHq1u%jK 8@DL4P&XnЙ| g#oZ&%".rIy~~'S#e\p60VF`-J2W8⌻dio|e`gP>(sSN>*1fW«/g=R1]{'ϚG,,.-=-+' uC]x 7Cb#*:=puOL v#&:/\PXS3Nd"0lRn5\O W8W/?x= 2 CTyz# =<'Ξߐ.CX߱''C%>|0p|CWC|D/O)ags&wo{x|/<~s SyIO00l?-߰~AWشE5O?)GP8'k<ޟ|tӞz>N?ɿDFy?)@~#hǘgr':P|#/{W&z<>ht'M J\P}triRJc/<xy2|b|2@y3O"z}-(P<7ۨWh 8tH48: C)227*bM}omomm{mm@ԗaGE ;_6@P!\%9(DUuPG]B*;@CC鍰AǶ8㍘6 NPKrUDȀ!0UNK!*ª- Ǧ/ؖzMJXj d]#f9cdX/k3PCQca`8̯q}V\WT5*RlU(4*45 TDQE5Qt\؎ 1Q]Uъ\]S\ثı+*1^*N)%b#D6(FJUVf*u&Ssa76V+5]+NjNgдGCh8c3F4Uf5Z.ыG* >=}c}OggS\BVXLL}>}>xv4h4`xh }}>x}>>AxkkOts?g"?Ph;mnִprk~98_ξ>L^_ : =1?"4dm~/?R` ə1,6/󬎧F=~? 3LGO<|D™luϑ<~c~ 믎}O㟮-;F<\>\ht '?%x0?PsWm, >~!?{w_BS~W <xA :} 'ufNbžtx}iQG(I;yCCn:c3B @1 q1QL86O_(w#h!SM'PQFxtͥ|t" ^K#w2. e'!_y l,h$֛xI_ F&wYˮ_^Q?8ڼ1HoCmPfy&_'sTn/;;eP]yGO~?Љw@9:9۴>98u{;f>d洉Q%Nu<928Q~fD9\[bV?Q9a| ~uy ~E"p?hH"d(O9΀50NI5c/zЉx9۟kMpjУs1/Ώ12:657GL8L.?;Q|< 2nN\cWs?~e9n {<*?)#OB`~ g*^qQNuP) AL?~o[:;I,>ز]Ui\9yJRYk&A'3L*)ht49XPa1H}f$ȏjA+\W(pf*cUhsUa9̪ːyVJ@47k/3K4zQ5՝:=ZJ :ΤPv-ҵIN~srUc)N%_X?aLF)-)'"=-=TLsr+l,.jp{B羒e(- ;GDO1$,zNLgxֳVGLbb7838)O0agHXYԉ̔9ZvsN}JV$$6,_%8f&Q,8APcUiFO,SR* O|b< 29p팾8#*Q)/$˰H<"(cFg2"""LH>'v3V|/Q} <3ng#o/`/ӒL糳9?&Z EmУ!0X3t!)se4x>?c|Fi@;XbC+FoO:\(A:;>מ~<}HP4|ϡ<Gtϊg) |y=|F9=Hs||o"=؋Hs;D.D:谄gJke;~I{< t59c5XAY)gBrBMmmmmommoQ/%8W8r]J@Gz!bi(+ 㜎S#Wr\t).3"{ͱP =(S+%ȪJ@ c.K ԅ  "zI;q޿v1M1-kh)i2 0Y ja.8 ViR]$ T=Bȸ7]F(c-TꦩPj 45UEUQauUUWU| `W ^JN(&W59'|JrIJ 0q6(FJF 1]esn*St|Ob9F(Vj1YuW^sS9fejѪ3QfH10B3  =v>ov>}4}>}G>x}>a;=4}@k@}>x}G>x}>xװ[<? h @{v #x~~c\t_"nG"σ_^ƑFuЈo(~}EOHy72 wRy| HI}ȗS3v]sYY?H ~2ӋOXv:wۯ'O L}ԝuV7]HRЁ[c>E,D3T<ͣ^{__]@t7'm|B+^m밿N@~ϟ)~Gm@&5h瓯M]& tǡC痎|cĨ翦1#uה1^C=ep'X`Bs7Je԰ΰ YB&ݟ|}rQ&GF8R}_(T ?z;Fڻy?ۘ˥P7` c90K x.Qs8nA a?$HrR#L?fOо/?0qh]O擼X^_=GF7_iGib?͑OggS]BVXM\!p̲79B?>>c~s$}K9 #ίIDB(ԉ(GξAMl~n͏Z. #b"f2qϗ,o)qſGG}?_5>!Vg6{;yq; kku݀'!xQ$>'y zض؍!H}v>7l 䌸.? ȣ̷Ĉ~m=? {cy"Nδdq̤ pfHe /[a˜6e<?8^xƭ6XFP(yЉk fnyF:}`"w̞SYi@vu$yK3^Py2Bgi~ddD9fZQLx#7: Z%COa4iD~ +*P^c <^q~p~(/Qh?b* "s /$c?r'9м0{IJ˪`@DB  ΕQ )?cKBۓ}$ X{qr[8./ЃaH.u<_w:Óbv=u"Qǒ;;@Eи|A4##Ckx(-V,?'kaukxjϲBJQB|ÝSE0tVW^ܷb K43fR9'>AmU$o5|G爠5FLj^+9J0,vH\cC-FӶ>) /e#4T*PO;Ϛ2ÇX֏3)jC-s5aИ-LtOU2|a6僁ZD &ol,~~0(OqBh!"ey-p|)FjkR*)ya)2RWs|?eV?j{"k$On*)u"ucAd 7nJ1<3Dn? h%=(AI c,10g:bRM92]5"tll 0gy^ض~K`2۝'T=MLtu%b ygtfϥ¤j}Tajpa,g'4!,VXQ`1K OLN}`LitǛgc->(9ϝȐcL)LJ;RSbc3))y߱3v-^B ?|M$`ĵ>s]|3R,YIv4Îjjhgqml^7CcݴeEmS2Q@NnZm2hoķo|g1D3Le#%tn֒Nka=;8C33eS-zw9@p?3 srzgNFQ(18Ddgr6su{ |w|OI:3v|ߗgw٣~a@ǁז)bx'ۯDlm;򽜁+C)1܅?'a<Sس'8jm!;y-~ϯ=|ǻ@ݝ{8֜yc/\s8yx);og8:П_g:W{+z:|<@<{}{)'5:P :?FN$dnMa=4{<"TNi^TҔ'4zyCyNp'2$C yG>y4,OG;_;}e;).Q/=SxV?"QY[* }e.EDJ b䪱PM-Kb"PY4CЭ|$}GUdH! VBki&e"AUSRZ->ٍ{ޣ=꽞+=_w_N#5}Zbr\A% Ah*k"のUj]9@ L4CPj"ۆhY#461|_Uq}LST3CBTj A.F*Eyp^ 65WU1Q T Ӂb.Y(F*]8'1C14tQF*3UVK UqSq>!s>!f#a6#3Qf0q_:'C}156Еfk]n׌fY XQ8YU @}>x}>x}}x{_x}>x>>xv?;O .C~.svk>kE}G~ o/8\h ZXf9ٵff%3*~9fixO|܃ӑ̟eX5:zsYϯcS y.|-ΧK ]Ti8:PD;:u39DoMT픿?Nsޥ P˜ԒL˔8|-;uh>k,_4ˆˢXX012Ϛσk:g98O> ψ(cXK<::ʣ x?IOggS^BVXNCy+ߙeis;8}_kpt9ƤH_Q\uVsk/BsDoHM(QdD~?))u~LG 93i<\$$2Bt)]P Sf3SY\dଖeTX=r>`QI'RKk*J%J$aYyY9%JWNՈ$+7,MW~Iì>79g4k5U:8L !]gL{K,,ԯs3xR V+ڐ-f˖) )'b)@Y(f)7W"OY?KgR`z 1ةtiIc$L3Bt3'4v8 [,&q}qaS(z:],:)!g)*soEM֮/>09u4OEC',c7*TʬG$*taj V]` &1f2 }R=-Ja9Nto3A$RK ul*]tm41#6;?Η0szwD:cӞ`|@pAΞDA$'m)&[Âx8)룛ZY |("I)|)ߘ? ~^;Vx;*P>b}}iT߁x_ڽ6q93r}ϝo}4Kz(Oh03Fzď9y.D ?ل~/{zkg⭡y rrë9}Ovy>82f$o'g7Wohj﷽{"t~mxO)prLyisla'<{g5|ިxS NϐN]aĜ m)7pCz>vdBOȘtO'ty!yޏ!``uUiS Xrbr|O蓈yJ<-8g"W | g'OdVCD_U0<poIc mmmis7lٛ{d":p@]wnSb:#r"@!RQQ\A΀Wr8G={燪L>SJ U%B+Sr W)U BooY]/zR{}{z 걜_Xu_Uu[UtSDԪUUYu]u_~WjYFŀ\`QuQa묊Xvz]mMr\8t]V ]v3|nl1hU%\q,3Sv=XtWSu-@3 4QUh~u[MzNR56V5_~uWOq-r5Gaڣe~S[`ܶtjmeZV1Xw A 184>{A}>h`xcx>hk}v=ƀ{`5}>׸hcx~v|Y,?7S~ 5 45`(֏p{@h4')u>pq0^#v:ü8ΰBhB0ΠO 61inM&g ~9s#O]yh^nssX[nG0/2̯ |k,'#luc2CK.lmQ~ߓ?tFªg NYeGhm yi!sSwϝls C?5P7⌬B"~klNo gFKhG y3?_eQ DqX녙?~\|ϒ 9?,8rNH8F<錇cΰ[#qOKϓ0 C9挙@rh&:49V;G%~u /y>|[ftyfz< 1+o/5vܾh Fx@~?cr;FBb1>w]&. |`Aְ ; p؝Q;0ma:c ýݬBu-75+2~?!tsP"86S'p qG4'_ i_u*}ha+̐Fpx~Ӄ˜(]Ax^?3\6~|yBf>QP3к͢ER5-gUdGߴ~? <h/Ll܍{Fl2_\~!|_% ?*$c`6>R?:1bgh^w,^GSa, u'cY<̮J"PF2 : OVJLrŗ_h:Go&>#2oH#lr]2xeKiEP8؅בl}:6!v߈ť؁hb['`=8fEc(Ts逓^GC)-Yac; [\('.Y^ĔuvCzru)ˤvٺVb%_]sILkes<΄r*QH ?J[~`O& Wt+S9jwHL|8H1QK#(NC1bs".1"J )?c?ghR%!,9iu?}!o 1,[ׇp;xdH$_;ȿLmg~@m[A:d5Ζ[)QBAE`Gq(&z32 .q*1$J}a)NO,H5&F02O0KG0ITuTLduAB2'ЏфDV)@_Oذg`P೗`lgD{~T{~\ADӷ*`q tp&sc'[ iD5sG z47/}_w;ޏMaK{>s '&<yO"5.lNB] C[HͿ ÝwСgO<<~=({>y$L #dC[4¸uQ>{b\ItO_q$ CX:JO!Roza{Х)ޫ>OȔ!8 o/k(`W4|D|M<$/OۘgehPpSF{m}}mmoO];l*w' 8]PxH>@C! E8!"E+yU< l|6:{axglUH!wU|mSBEm EI:F nyz$sCH xB n77ma)gS(0 DDTE\q+=WSa7ŦJQ ueta-&jzFTPbcbʬD6=ǹ\nǫ>y/~*WTX"ȳ,C1C6hVl}x} }}~}ƻ}kxv>|ϼ}4@{@v;G>xo}>c~(}GxhZx5xm{> Og^ƽ~m>4 h #[6}؆|| `F?jv!tXnGW6d]a\Yk-irs+kkt mˮzeV7'h 8||1 ["?FHRAs=it~;x<,ͺG@N?6_yky3S-añ7XNg'fDGB&ee91{Ǟ#oy&7~p{0)x~d~m FPtꞳПܜ.b-pb{BNmߛ'X=Iձgt#hK3yz 3~H9 ,@(D?%Хza V@j:qGLhcU0+gu3k?>oHDy_g'#rB'σ/1Q3A Zp0 ! ?-a?7_eDo#} ~qyQ083*8Di6ސ]k-=zڃ_H(zX{?^t~IYįA,#?hPc+ BA #/`xG_(BuB[SA Om!1sZqV16\<|^t jwK E[]LaCuîqu#G1ͅ$]G?E<&9|=9h]S%0AG(hqa (Ti]C-yȔw W`Myqg0V|y/1pzPFA.B_ΎBKbu=:"t"Ûs둏#9t,9UJρ-OEqL{Pt?Ac5C9,yRB.Q!sL>`s5]P dX&\Z9v%TQbZc+pOLfS8L垢Y4LԟYЩtNf3.,6ڎ7+Ħo~apdQ]Y;7FU>3#% _TfYc$jm4bcKRLXs//wҳ\~}(t#Ex)aBp%$J΃mRV*}̩gUų̃"L֧$r)$cdqg14G9)(ȰqI32JIRSvx}lM`Y>`$4$K'i~D!j0ZY̱@O `c^15.ca,ժY 0aSĻAF 09KpKlI0F4K x.Tck>j &?y<'J15dI^=?VCRҹ%0bc'iɄ>/\:9FeobӤUxBkⁿY| o\1?Fݟ+5us-js\mG6Fͱ9xo,! $d)Kg[|r5yX ~|/~sj?OEiȽG9~9zϕ{/Q;'O#)M>Wݷg~œϊ~r뵟qP#KQ3}oo'}06"M^G{>:>~b|L<3^_MCBw ¡rf ^|w0"p}&_/C~OW_;!UL!^TD[B<=+bψ=s$% HrS b<aA.{球_g@{0ؾX"B?24N}}5qg;!yyBiO8iᦡinr`ROggSaBVXPMw}wwNu/PB@+ T=z DF ؇DU4"bPMa & P#sz=U}09RlBYB> 1XEl!*%e%lj´x<=۠ޏ|A l^յ\ReL45MtY1bM9kwfD4DPjYfv*Nh|q}Xh]4*ASUEVQu#X%Y$c8UuBs\.tԮ~jS朋02͈Th%YvL+N)ıMFj3UbE7Nu 5 1E(V5Yj5:Vq׸ cn}>xqq>x=}>P}i9}Q #7A~>7O} ~@hh<9b?lv_o/Y[.eP/) oQ?ߗ7ZnITi,'`0~y31Et\eΧF-9ϾP;s$le׎uD1 ~"'ViM#v255<10/9狐W>l ?/!1)N䅬4mEmu?Bd". ol!мԃ ~g!>b&R95~ls~)B2Y"G`v(1$P'L!_?<?2Co&~7ؖ:dE#ho#3^_3 >uS` Bd4<{HgRY7S8 FbD[ mѾȷ^~ۯ<bul-oanlM' qDtߓ;47c :Oj8;J5xfqqrbsxfƲYC N_F'mo;o?<'oljo (Qc F?Ǽ_zs(>v$>a\̿"$ys;7F7xLhABՐ+&Y/"gcL.N5?|)NCvۙ(M/?~"7mY6e>_m3?v R#y݊4Xn-3S՛Xq2e՘ѹ0ۮ~F8aQр Ns? /2 a0ΜD|| stue'P{9gj}< GmDH5g.s-!᎕s[\;@S1^ة( 8[q tAzAEa,H`U4f< yTPǓ/믧|0DOuXR`~釖:6/`f|Ö=rEaCІG"G/onwO`<tϹ#~Ϻ?a}՝\R`+?8!A>Y-sHI<\ 8Dm̕ac1+X8΄)ӝ'{{m$|¶Dɐ<ƨw!C͖sކ(u?^{gSopku;("#rnܮ3O[!$Ȗ*s\G2)l ?( bùֺ~d}'2r~YvS2|2GYKY~eSuXi2_Ir9mϜG#3}"V:m +9c\e|3HYQL2:[lkd'櫹Ԓ0F5&KR|uI9B5+UTZ429| zǾ3v߹n'8MS2tqP*WG&U֨z_xq:s$̜H꫌\=.?AF[\U p~K,],"3<틅,HyXFG`gyHcE$Qp\Q:Ӯ)܉Lanό9)~ 4cyK+X$e[}܍Ih$r~`R[-gE=`䓦}!,e-;mc<0yX:X?u'F3Ak |m.V:NbK;s~fcxЄv& &PFδ'$39ӬŚQy3AF<xgloP6gƲiDbu@2R'>2.KI۲dK"QeT9C:p[-:#S jxk6NDY]*5A*Wԛ.6$p!><)o2t䊯<>z~9σa+DQ 9>d8P|OƴNLs )_s10O_Np AGM zx~@Dм?0;.N3r u6-7yͤxx#T'~v5!!<) +$п0k,<~g {B }D: ބϞAsy7v|aX3eY{DCi 9t`>7X{C |x\tZSv־D>Ҋw],z痥J Ĝ"P#ՠE|^ogʮ/׏ɓ(=|"~<imd lF]>\8>3W?F=8 L|/7Cy{<=_t9ȡ:^m:}"gFCXr,(t9l%N9 yB}q<σ!o8"`0DNƯIF}]B:c>h[0H`fm@ @ZQE - hhb؀@&NJ;i &[i8$ҍ16ir~Z #m&-6Y| & ii&1&[! BM9%i&dM|蔔mJTI}DI䤓I=I$i$Sh$H$QMji(]$[a&ME"ڤE4i6M$$) $P@HjV▄%BDF t5&MMFM>D NFi%m] M6ԍ4r4N8mH Mi;m J)Fb6m3&1FM6(e[m $I$RhA,JI7Il 0oi8Mk7 8qg V2{5ŵZk4&KaPDAj KCU`Ad5EQϖ2݌aŢ5.D(D}54A,jiߖ2Ŧ^okֵuªSDԪ =D DQE뢊",`=Ya&蚕r\рU*/ۦqSuw[n&P,Or!p1㉰5тaطx8b5M* <݇-^INI9gȱ,2ql͊ Td5)^Ef9q5^Ѡ{i{ϼk@.khx;~Ѹy'x8G'“|O'-x*Ihz0ͰLkXygWNHG`x{o}5) u3/h_S~h{=$xO?G=!6edhߖqLqٙ.9#%r>_K21$#?po슄Lp9{wfiϊnH)/Ǟ̗O{NPJR(Iw`4}ff@q~3MdssMHZAJQyq4)JC)fzA0@ssgLr~Nkwg4{uQ'ḽ8P<ߚxa)gcf8\GHq98*Sz9iF:X|0)s#wҥdI0[jbį-IˋaNN|#glVǓ7[g>UIT91LLzAɞkW'#۳v\R56&\~owq2GC=Ϝ>ݺfvжn}_w,v_/񟛷iȖe<yv~3<zWc|riu#fqT#EχO;gXs0R<1-6N<8rGL,湳1XsSH? Sb'O*<]D2qNMM=iQCle(đ1 BK/m:|coDG߃f]LҊAo6&iT1 q4w|w.Y8RT:k;]V&M6 */H38oQ ;l^"Qe4>ۦ56xyB"%w34<N>߸>qi~ox5y:o7.\_?σO;;yG<|˗ 3p#+d|NJPQXǏɝ Hރ;fX`?b{_?3W` r>ˀ߽GsxxOf!1~9sC91vf\9)yľ2SQ<1Z1ő=H}GDGs*}aĘ;T;,r Um*pr+zӌٽ"~dԹ? ˏ&ij]V~?THvP}"¹?EV͋%i:GnoS;H.R˷K\96iu~%mB9^HbIY!L9N2 ߒ% rd?>mNu.nT%'gVӔ#wIcɶP|du.W>Rrݺw=)({ ҙ9s͆y߽~Wq-}^$hMU=dgՙw{f0]?mDQBc/T99ߩDs9B C>QE $H(JqvN.NNm6+zNz>uO ם)m^ M~ >M?p&R̜:$% ei{W9( 97q|AwptaPW9r~r6$.mc2IY)\u,[zEkjW.E u$ͶNڽ4UmG)$"~tW _ ?/$8.䙻CGTNO7oAn2;9ҶRLju5Jh.\=Ρf_2|:'gHrެ%Q}/A9gD`a\AH~֩t<"e\pއ)* +\!l`ëf9ua p 8ßc=9dA{Q%S~c>}<ɓJ_?eǕ:DrTq>g2.uR@WB%NGoc*᫠o\t=" N XC5*ė#T|%ϒ$;X} czJKyM Y4b/bTu-5yIO}noZcO R&Y(B{?X*P3F٤Q%RB`NouYYTRzt5.:Qzt7feΧaX{"D0D,T;oHZUlzpMVzzꤌXf'aLaf[ )0pڪ X[{l-s)f1%ŷicn<.cӤX ,+geX3q>Pȑ ÎXB.Oج4eH\6E>IbɮԎRINՕ?b]]_*lΥ:dJĽ$g8 B0rcrlM 7ED4i2*qs'hbXV;mU';7SDP"{sq|1OsOo{gI/\v󋓳'JꤡtS0YBY*$}Zw).!8ﳳ̣u/Nb0u-JI$1Da:SmaUW*=XgVxËTP購s'dp+#+m#(D9Ա#vgx㨺F=s㨝G|>GPo@O'}zH|9~mDJ|ϝ'ȟ!x?30|+NΌ$>L4Oګ1ӯNo~OT~i{K^ D|Õ>х>tx~T} K%0'O bsWNs/CwŤ{ r|_ZR>\+8>gOE5XLd1!gX39뎸nvjSGO<=$3n#@OJtE2 H&:aaJww}wwN}`wK=uP$A|&đ)5{jC?Si1yvmb79D c Qo8Vd\&`-*RV^|m0lW*"eUEq0і""aX+PN*N`ۯ^*StN`_շۊ+#k`T_E%qN)ıMF*1Fkny&Ȁ}h}>x@(>~>x x>}ǸMϼ}Z}>x}y(x~~쟀+Y #G)g47=4=+g?.^/9ʎv;hI/g#߸6d]|/Y.GS?tj?;Yo"E?I?)>zÐNlr EγVH',[EyR& 1 }22a!WSrZѳ✟,`݀ 1 ]Dn3e3'3’~ci𳘉PVXF19#Z#ykk x8vD"@#0瓟]~dI<vb,?/We//Ј&:󼿑/̲}h|̓l9ۨ$NsvdonϑsǜyWxcv"$|?XDHon{<ЀPo/_0 _ƴ,}I5Cl3jt vx%o1Qam vFȞ}xS]~ŸRE'"x4s xQ0&нмwPeDi3m؈ ևag1B;0[F~tb8<o%  o'y92l@r92T(g y^g~2jCD}.rʐmw2o/ͬw"YL8?d/gm,?2~xHi0u\//'NQc(aI_b(!jnN>ףR3OL~nA+a;&r- jeF%`1,o/oSk,c~& z '>&׆^gmL(SQbcB<?ރ'<6&Ϧ#? {fY4 #sӿ3!40p}; yl4gm91L܆q߿|ƛȉtS)sprD8qq3&&đdO3~bQ&0K<gg6>" c~>N?6&d͇GOsNg<~&6qßhҏq5sF?Gՙ2%HTXKQB9L5ǽNtz 2jsy$N}"A>w 9Ԛg}2TyU+e'| PSJWٲ.`I~{zcNՔJBxbX&d)88;S/u!R+7;e~ZwSChN]vtk:,%V'U X1c _X[J 'Wm.{ZvÏ|Ej:u3$2~3$Md)kFu}K3FyrpI'?LVisNpٜw9~Gueq6q\ʁe}:uTؓ$pO]VdɒXrJtA\=~lBRiN)и.:BԙdduFG Vee!rR4ege' tXaժe1QU g 5@g&D9Bxz3,&y16gQ? ʬ笪D谜d>#oO3*T`#̖T|P!>:(;O~6ŴjW%9d_rIX֖C4OY$LOTWpB}7Ԝ\oq1L"β@Im4K2vhcwX-B(Z8F4+9Ag噺y.vsR™k q8adY)perfX 7< cKVYIgIe-sΤL&V>kTФmtdO !TD{ڑgJ7~ƾF"8) 탉cp,KxRQa\c)%byeD 3b@(eD̓=$_Jv5u':*`8!JM@? cM\7YD1[>^S70Z YgBSBNtK/0%XǰPFHXS+m6Yo;% U>(O? ojlҙɍ҄ /Y {cGLiac9չ1/3c sGۦ|I(sϽ`t=;=Ҙ{&e9U:ʌ)5lTms'9H ]%_߼~p*uf9@p^MW8av,.`f&hK\pu9I|Nj#7ϋ >w@|O'>b!y;Vg}Χ}͵q9~,N~>GOt0|9rOx?/C埑~' }t9>:juǃop{%xNp8)z)Cs8|D3<^}"tcNipv' v^e> V=|0$y7b7>=$~>/ nΙ~SѯDؼD!PiOGڟΒWTu~\՜̸^ íQSwfݟ#\LosGOOaޞMoi9kڝ׶*3>IW"vM`A_> ha U@><Co xBY4_کš(7~&ĘY9 ϑJ$3D5pAtqEHqv>|y:;wRuܽ}- D*:b(X7eB%>L| U@"IDՓ+ch=Qw@aB AV T A%mަzTD(@'m 8޽Ǟpz`T!^0HgV50dR.]"@$AP*6R^S1OggSdBVXS?n.&[Wi.&D@,Z"*T*Wi&vA1Ĵ1- A5Fx$OR]]Wp V ^ nıNأQb%%y8{#kb_T朋!؛4d%Xٿ> }G{Gcx>nx>>ƀ){v>}>@{>xpxv4it{PC@k@x4h^];~2;}ր]k]yWIm߂[h`#k# L?h/8&7[db NnghQbb8vmnt_'XCζuWѷDdO >2~7:`e>[YGe';ensd}f |؋~u1|musG3?F|Ј ;r8e6>q8ǜ_ÜP_C͛Nd??BNP2?;1u㭼G ^JgSH~C^r&0AcyQ&#K3A)_ͻ]m??LZNQ> 46_9b'>fAEL3 :6GmbMߌ8Q3<3֛ߛg.p '|o[BB8 /ϗq/0tf"|8ܓr޸:_x@  0Qsym=@6AA/1:%A16O?mߢ&G? ȟ2/ G=G[3ܳ5@yo3#81#6h5 ~J7Y0o,o;/.~z0 cN._xOBѠJ3Ùʃ<:9gnO#l߽FY;(egьccX.j!к ̌{_ ?L ?k?~тs#_Rvz4Qa xw"E!:8.A`}WG=yĢE?_rf<ًI~>`! b:Vy<Ǡ|f~ y]OqG](:x] or! ^iCQТEȐ~.@b}ZO`JGEkLmγ1MNOwT'V&ą"{39ЫFOLy\q61u:jqw4|iza9㊌yXL94pWYgPFLZT\+?c&rViiҲJG9_tYɥ#^ޤ.͟9WuHS)'"Yζ5C"y `̱̇n Oy~;,v]]/*sPsXȬ RNGqȿz ('X᫾哜`cU W"Ek,/ D^n\U|Uo]o|fPAoBiDA<*C0K9.߃Jd7-aω,#8FgR!DmRf]bR͗BnI`f7S4`' 7XX,01ԩc3R*+aYL䏦gKVaL%R8s!I Ij<[JX(Ntɬtp$qY ,\>z*YLBRFpHVp,Jڐ>. sC!e<6AHWy̰LZ|P#YO Be.,\]bK\9+Fԭ# ^7Rg, 2?5,s:]L7tʨ91[z̟Y3uSҗ0̧ UI5U:|>WnpGW8pTMӈrzUD5'gY~J/<"O DϔRs1k{'|yrsy19mbco>gw l>RKN)؟ -| SÝ_=s@ޟiXvisD"{;< z|(we7;C񼿉>{"v,\X,{~cO9azK>D;O&>4$&t*^xhtg'yLx4wiAɦH V絼v<}_f߁g6++t- i}N֜? WC,3 T{VZRGڴ'{WBFA:>t({>.fW80z2TᠴD@ǣ NM,ﻧ~6f J>P'YA6T)(a'(D!= Gɾ@ -<}(gD!Vxu+Pʀ[[AM%MzwWyDo3;zpyӹAq/`8q1idr`Z 1D5ÂnlKjbb EP s,~/aDMh4(QUtb1ՆlE`.Sp#`lQH]USrb(0QW`yNi93Y##f%vkW2sFj3U|њ)hy~xx}>xV[_Cx^oOYx0@׻o~M#q $? cy/ߧb(! O](^|ƲS`G s0Yȃn|bbhI,~m\C]'%#y;72`4CyƸzj['~OHsmRm֟ʍ ɒ6m eQc)`#>L?U#OGؙA^Lnd` d 'GD|x@@| %jAל?~Yo5Fۚψcfv<'G31Vx%oOs"k?RFK4?v2?,ӫ<nO?nVlrఎ}̿(nʍFvzӚ%W6 ;'#MLy"wy1>NlOggSeBVXT΃*6Phւ"nb:h.ӌ.3WY&7L+9Yek9n"ǀk(9({k"5;FilZ(1f필3j=}1'||Oly \y1ErڎHf,GmdDDfB ?N'bGڼ!|!L,} HQ'V`2Zz}~SyQaxXy8cvLԮQߙ"|b1_.̿ͣ(o7~cyNmq_>|#F1R)(|OGWA~Y/ L~v:@[y?x< q{u>?_F<װ(.? p-#_NJL9Q# Ҵ|>4XQΨYJu9Fl$kyr&:Kd-Yzt0:]+>fTt(,N}vvNA]T)ɩfa]OQEJG!|-?|y,J9^ykSR?O29Đ*Z9R)Sd1NRL֬9 ߛ;|G:UQ[ Ԅ3T'_BX"G|tdZI9g +,  TqQLf53%㈲kO 0ӔacHZx0Q(QRJBi'U+cbcdceE Kh)bvfs9gk}B]>1SI%01wfj4&8pv%8֞e7VdaႷgb3qlM]0rƼԱFXI\t+<6.ݏEe,)?^8we5j p#gs3I;:y'TM2<Zr5 c1錺( # a#> V|Xa]\6Иw?dVsa m v|CQ :} ,MyfXzࠣ Ђx+k4>Ǹ}>x vx=}>x kx4}>>xF{TOyz{#x}Oݠ4>`p{> <]~Y] o  ZS <`>^m)hkD{3(Oa WF&ȟ C|OS?0OI6mQ'"OҧoFY-}d&.LI9d@L m1yb_pHz|""k/ہ;X#YxBO#o? \^ߦN ?.snAËcsnPaQ;IR2̘1"5yn_oy?ϳ^w/r_Èg/>f3+jJ$Zj]L89/ 0Xqut/H"lSXDL06hQT;=ڎ[..?߮l ~܈c<fY ۙ:߄y;-Zn0K2G'y&mJ#ߣ[1Ϝg4L[/'$3+o G_hRlo:0`͍߽pD\Ϗa; ;w6RyOqL?9EH2˴B?x/=ZgFi0pZlψ ?Đy$~caG~lgټg9euAg$'yįE2_94vÍ@P }᝸a<2?ʘBp#ƼI T/t(cqF.GGもD,WD,tJ#.\1xyziZw;@ϗ'+y93DߏNDz#6*#Gpy0tccSB/^N3bI>4)&&)#x18~#gI~ğivEߖ<ۈqv2녅X?:3jQc.s9i^<]x '9V)YJ̤4E }G79&E1i3w uo)"bӫ魦0tDP5!2ȳ7eDKL* OggSgBVXU1Y}&X9kavPr{l;sa+gUH{s|YTH(9$]b>VXI*,Z\z{wu5#`-Γ,ώò_X%()swi:|a|a$Â~ hY.{H"SAOSϝc4>kdD)ǜP,3="PN`uHΎw)?mvnߋΣ9}Bdްt14mLts澰|apB2ҡFd\DLUZfՓG^E(Œc*`H#2.chA?mCXQ0XVώ~G-۶ l젥`GE$%җ=3XkF&`qRgXF<ςS1gi*[C 1K,,Y9:\9⵽jBMFXcSpŋBBY8qqcO9!L ۚ=|łgß*?8'9cgS(k S#@ReKX`H,0eplexk32 +T̳5 `\g(QB}GP[BuFv [˙ф?Z»2iC ڭ _ЌjBAɷ5>("ETicez,7J|$J]2WPpI$z'u4*8_],گTTMQxO7rt⒝Ny޽7'>=?<3 BaQ:SD>QӃlPŝq]+7sO'I:MZj #CM1l~|_B)ϑL:4/_SJt^{uyޏ}szO#q?gsN|ϙ@';tO<= ^xx~O'Cϳ姣; @x}r?ωt(yCow;؉<oW`pҭS2v|$6rr8>-9"5I'yBsΎ)xJSU$O!23+| B{d;LOdBAK=Np< TwIo}}mmmvcmﭭtmuMݻm)ȀKBy9@AwW w&JQ^+O]:].(Q$IЄP8!'Ȋ\]( E]Ї>S<3}+J\Yc)ʱ|X^-U2 BTS-5bwp0"cQ\pC(s2 ,ēT STl76r*(@TB]7+p\n6EtTU}Xn\.9.ꪪ_[ bl 1F 1UTb@v>xh}>Q>h}=(v4v>x}}x>> _2?G~$|O ?Gv5v=3ň? 7Z3]| ?!?E2v߮c[B~YߜSѭ [dL OZA!uY!?BXk (e?2c # tx~Nm=mL~Le`~rۢz3~l73LmxukHؘ\_hmat)7~Oۿ#5#ln7lGZE_shYW`/jA~|s#Ѭ9>L}QU̍d a(f`B ~_Ah_n !^Rg/ż9E0?~}a4'm((͍(a7>Y}7_T4 7dvZsr.m7s9cg?9bcmDńp)euxfQm~ ڄ`[A@`3~?nd: ~f@8\;D1sK'f|o3s!~Sb BQ.7vۦoB;jLmy3|0O##JLCZLۆ\;+ 7κ~?T3 cZA> ~cp=8ٟ xA^mL4u9:A1cbwL``gjIeO'~N!O5}"̟%sqO.gJ/|f5"K>ev o'Sl; |N} Q,QmƇyc{,!Z*G~#{۽k?}8*IZD?aKbęxvgK:Ԃ\Lld3y]Þ\߈޵Y)QU\r'J{?tehɪ[nvuex<ࡗ.ZoFKyD&<2Hۚ_YO3%f1ܦDH)Wo+O~iчecLʮn9;fk>&sy%re}a/ Jjԏb, 9*QbuҬ(?K>fe߲}5uiu_0 rRqV\sfuIJHx~e]^>.wV$Qs㮥‰/xvY{L圐G;嬢y'RrHu#㛐p"vK&Y_r8XHZ-J2X& tby fh'X,"g`f0Ǘ&DqD("U1׾MLy냌"{#JshXa>puXȱkQV]U-b2 )gnQ~1s.B=F,k.+&r`dn KDI=9̱}L`$?vkZ8j7Ghda7]Ь0B95P Q#Ɖ$n06<wq0Ja9]L;U0rJ5%LI8UI#t)yMs!|pnjJ;Y-Ul"8lܔ;8wzxS9IRxNMW鲝N:"GFb{;8Z ޷?'~;SOȡ9;瞸7}Bu_)>'sZ|8>o~!4>'/? +Y(yeB/$|y:/;;M|q :+O>'S=ϩ+ggs,>y|>#aLoROΟgVv|OFSq) #>ǎrGx=_oD9NDA$?'r;{4OggShBVXV_syU{Sv`GpCH05>BC|Ͼ_wq2@c5(JƟC>m}mmmh9bA}Wt)ܪ Ns:z{'JSU+70ay{01=͌sXw rwA{{Ҁ8]] l6p{}>y}>_~17i?/~98 v{}n/? \:?-Kp˿^_ :GA_s:GShksy2lLs8nI"vŷg1DeYeczr6S(ɏT^2W29ۨ4721꙯XnI|p"@?qv6>?>O76iT`c0ۏ9)"W=6#}/3Y)ejGQ2O/[|)'#wϣA7Q̡F&<X#'f~, :d?=_i \p֌݉SQ^Bqx=Fȍ^;V(1 -?6e|LyQy3o#=zd&F#|}nPGi?VI&"QЎT0]{Cc/1 cT\x[II;|JEQ!dN:EeG[=?r'_}Ho_e2- bӜ=TzV?(}_F:>d7/$H9 &GSrIq}Dr/4KwYoY9/`߿71LiP:ӟNtmyH =?2FTUpf.NYw̰u #Jf$3#Ĥι/$ı, Abߋ=~/R%QZYfhWps}YC/]:xIk4k P‘JhAd4O$K:H8_)H-%:F ~c9~zK]O„BY~y'IdS Lh},*_'%/{zl*4cLx_V녔5wh%a-~C>yFRT'L4|Ǿ( 2>1QKxֿ3Iτpeȥ$e ,SO1l=5+O[?%#R99HOpt7sOŤaiHLHx[FdVdU?p<~HcTaXS7OaeBPVȊx4UFe1F~DZ@HzvSgdJ!(t$Tg jz-~Ҋ^x&fFEF['J16$Yg8N"-̠pVXنWA&.bXlв by>`eĩڹ N=94Q%@'HVk #`Tg 1%|u$fztNttabҌ8:H}0TxCd}S$fD>F/=)>}gVyy/<8=gH~}OaQE>P}.z8?퇇?So8U; =o Dv;=3|΍ZNͽ_WyJh>9oC{OъL_g}B zu~gFx /|4 pȅ0!3x&W/uS}>@ x笆؆ws |S½Q HT{ >lܝ0D9L|駔jB'o} іݷT"jT TP<+S"w rw]ԠTl9)3 2)t\ Ruw+" HK@'s {!(a60om*z \{bIHr $0EP*qmZk4.AL "QXnXYkjZK "Qq5ⶭMXTb T*MST*OAT(56&*((aU*%k".j7=qEuUUWMq.@xGx>>x }>}X}>x}>x>x矀 b~h׻hh{Z;Oe?9KHk-4v~c-h+O֒y/`{g+c#/? ~ V~A:]B)d#-BQo?X[޳9(Ycc"r"uO~!rY2eO?fz~bb#-@( .BܡI1}x\ߖ=S3,ߌ3f3"b Nz~%/쐶>]y.wc._)>;GgB(=`~K[y55-iE ^~mτ<2(+l '><`v|~~@ XqpC8dCX&eOe#91e!J?woއ/c3ۈY@+ńK`2dHu3џgy`GSAOggSjBVXW#*5eǢxbX~^wRnsDpLyB3g/bq<0qp?GAVBAXψ)ϼD|e1_$!!x2Fn)Փ\ɓ8C' ۃ4%Tݬ]jd'P9v\Lа}!aT^5 m)4 2rvv|쓐dTy@fii9 r吘Ne\EĕY!l[H?C +>X k?ZMg +Sv fE$̦ip*cjiFe!oz ȿjî1d3xXˬ,q󫇉:2O9C $jBK a$hIjPY4>_ 9|ޗ+;ܶesI"ei*L/FTǥcl` (1R d,3 g5K0xϸa:3`=z Eb [&C=N)J'ФXO{*i0^6RHc8c#tP|aA{9@>&S^? 솟äsA{tNyyZv,Sjgrlf=t|} <ɯS>sx^΃y{){o\Osk{_ν'wSÇ4/Bh!Ït|pS;>x|D5Z D{5Vy>OgQ@=?gN'ڞpY@fz0~p~' xazpO(}vw47E9kNJPy'k:Y, >^՟'yp' i@aY4:AR/ﻻ԰1lA(I- [G=)AAtI1(+E%Ք=)ۃ\-ahvD$E,hB(M-/c8jsZkUFj||oX~VUbHLr$* SK&1*_lscUYR:AJd B1fIVdD0h5*Rfc5<=SF:+&hhzAh6\WU5MSRPTjbEzBa(]]q>x}>3~-Bh4|O&p۴. 6\Y2G K`矎1!B$] <MA3y;%/?_0M>:7Xf/Iϱ'"?zqnfxѶ nk 8s%sfug9$߻?3-i@rٝ/sFO~2FAښ@S`E=I}3y[x|~FcӛE"g (~efhD>Ot &/)Bt)9w Qڱk0?9~Aזf6Lo<ڟGO]\ '\zȼqPݎŞ6A6ɳo3vOs?E0(7'p~e֋/#Gs`nJ IB:Ti.H?4dh3ٞ#a쏏r$Omy? Dm/P_(~Lj~ &DgdFLBShc/!D(_i$~Ay+fH\3 `.2呿@'UϻXg{ s'_ZY9 O R$>Sqg9fuuи~pQŽs?BvBſd{x;(]P -·? a:щ2Q4ߏ(G?yEpq~.)< p)ύrJC_ ap54z/8p]pCSЅ߹μ sc gbJ4,Gxre,Ur{c"w+]7e:5SPcIgE!Ed\.rtB_CeE`k"ԯ0rN)2Z&gQYcMxC)e(J1r~ jl%$dr)~,%\3C7cEcYX,%ks>*AT_#Im4ǵ$Rpt|v<d> S=vgu_;-d`Q*tS;SG8qk9Eձԑ<JVxE!9Es*}|٬'>Y='Y) /$pI}a%}?RX%}#n!S'|PFK.䓊HzP6VYZ&#*e=uI%K '4llugH?m~5>$봊I*QCN1qRJq1L3IY; 9_&#kR^k"J,q9Uνx,1g=g[1O.!?WgZޏ} ؾG~>'>Ň}S:/1'=a+=i:H1} Ov|n8|$> 1xLmR^q/0=g||o\Ng{{%,/I8ys0>~|_ 3Ÿ;S|oy 4_'Sn]uǗ{>Q;{~Q:(wv|Óp>=B;>Jrx<ϡ~/1zg̥5Q}W%Xr|&tӂZj)>Gw ް9R8hd{a{L(]4La[7_U9@II8(9$3}ommm}vwt fCT_]Ԩ5( m*Np]U@N D\$}E3j.GK|o@PK] B<>=@ /D*Ư굎isYKwp(PhzOs"j֥:A$D !Ph k/E&Bpq(r mU]4MRP*(4QDQjbST€F MTUDQUOggSkBVXX0WU5(zFSXETMƻ>}xh}Gkx>v;xx}>x}>x}x>7OqBhw~© v;Y_sE؟FZLS>+0Ssv8oy} z: ܑmsno;v3тTH&N/#! QW嗟2 a?n̙e~ Zo!lR:{ 3@bCR?0 (2;yy^ ;OV`[/0A(42e gz !]eyn/G}~2>av%m2 7$?rD{Gfs<8=k ,O7f_$ހ?>`|H5z,X}pq~> ݎ]c2=>GdDY#7i57bgȘZ1y]B'fXHFN,Ńp%676+rO{򿏽Oc3`z1#Y&~6 +?~"oeFkcѾmb>I#cu;8G"`Lrs8O?͸Nۓf?8?=sfP=DsB c(hV|O$´'-~OO(?$ WqeXuߌ&8Ea}|P&D|">J)y SV!em!+G݋'ɌJIaF͘c'Ne:bVgnb֚\tV6dY"zXDηGEg༴>=9gȿa}׌S(zSO3:~!;|-iR@~O|{?w^U至_&r'D(wKy ފpWsz9P4C9љ9x{,Ny} OK pp`w/ꓗ+` p!ضzH(u>\6i߀խ+$V8}$}7́=Neў~GwȜ9BQ经_y Q_Μ!9}^`|oJ; W^{^‚aA]#@pcUgJwJțp& =疡@DNF~板yyz yt#ZOHR_=%-%4 gO>`㎹BKt8N%abpg.'ft'̦[;a\yU|"+ww{{{{{{w{xmvA{_>_]DSBRICTD@_t:)<|!]J#@!QS` ;wqێNswww;ݻ7|Q$}|B)Uww;3{oom%" ;`>{A}}}x}>}>4{ z @ }>xAGyv>9>=h}}4A@F:>hFc\?]Orc=5 mv4vk@`(P8r~I.tbÝv;O0[i`\15\B̸sv:9>$]cyZݶ/>3e`e3_k47apWT-)|5#2iiɣ<2ٙ˓_r=ߞc,/!?}c,Xeb Bcy3uGnrHD-#u3C}MX<6/;3srb_8?ߜ?|ПqW~/bx+y1Ŕ_8<2s#r8ӣ21xG!BD~#,57e*0}pn,3 /}ADlCo^c/yLj 9Οi@s"\ۈ>8OvCMN. 6!saD ~?_v7>7?xՎ(I. _Qf}yǬd+?_ׯ͍]mʂ<+^w ۪IQKw$o>".r{|MNx$JTVz5_O7 uۢި>9 UGH{u<iYhs4 `\ XDq>ֳ 2J!OEzWL,MIyauꂲ|S)gkcV_ Uk_TYub8 b~kyw'O˗miaoe"_ŵ= .wyX[=_ ʎ(Ξ n-y{z}OQ*3Z]6[6H8 DkוZ4#>TQܴ]Jr5##DF/]>GPuQ\E,b:I U!i$-陞ɜqfg^ڞt|Ҷ)!.~zE2FȪS[ލ@to"sUv}Y1uLmZÛ6kHבs_}m>.cm>7&':ҳA[yQ` yO'[Urԭ&ҋ"R3HQF[>8_}J}Аycō-\ec;, V!%k%HY JՇ/.j=it_Yd=RH,9$G8ĴcawL+~XMyD , J$I$ qI+.xؾR,aZn=C ]k9ֺ/CZ@QZB p+^@0G$>n-N)@RǓ$$XLB-{$A+ w;x}^/94F?nu_tx|'H#yyA/|t c9Ni9>ܝFod<ϑ/tE=x>gp.Ja3Χg}>9<+}OT?stCd;SzC8W|"'J=xJA:<dO(DÑ95<9yFA:x:0>Ftyx89~XY94v߁;=?!1lTżC6G>W], O`S: CNy6>ҡʺEB2y\KOTG ξ3yjpL2ggHp}D;{{o mm6*>t!EN]RAKQwS) T j\D+帾".`hQ<=.pǽrܜx QsE\>|{+w@B0 ٴd[9#fx {Vc3islͶͰ!5U8 M6lkf\<4DiD¡Pj+ ޭUf)yES&vPU*ŇmU^U)E@BTH@r‹oͱ]15"ACQ_uVԚj<"*45D ꮪhP MDUEe5n@xh}{{_x}>x}>x`}>x{ k{x>4{)F'n;wGGkFvF==B?>O? Pį|^#Ab- {М}v*CB)_,~ ^pש=̟6AV't6: ?ڋ ub'YHOKo De>~~s:3dbb߱;}YY? GSrlH`e;~ Ic#)"`me_yzfH= չm9d[jMDR<͢6G^"Pb;;G3OSh1|'Q+[fCg"}ȓ0 pl!quseF&w2=o>JAg1k7 [}3m;|'y9n|s^{69O1LdVS32R[AH"۲nH'p O>fP}(%wm_'97硾Ȉ)瓚pO̶87FC32#=j( :Ѵc3fL,&ѡ LNo3~]]#"&S6E 61lY>q0Siy/;hP"D~~6fYb@qڛ({fDux}YG=:sg룮CÈ#Hc =u OкB `5 ByYVX|| ~Hs?!!31+P!zwB auM)Or`Q'8Xԩ,/.A׷BTPtQGJT\*V/Za4 B'g}m7`W-v꾈lmDRPU*K^76 &+Zުu(pZdL,/uw*X5Z=˵6g_X^.k5%!u*3v_$}P#_w"հO65o(a1Q*64nܱk`1H{ @IH5:|)/-D>p[@"j:QHĤHX?ؓQ$Oa BdrQtmJoyu& [򇣨bgh{׭/}݅k>W\)eyPQ׹Vy;gʾ`vޞ|%0O2ek,#r];D1nBvJEǗO+*:ZceI=̘UkS5b|8L:Up"VsOWݨG)ds< K"=Yd,'sKN*ah*1]VHNDrv'*no,RWn#ZAsr˖+]_*Cs9~}f#[%죇%Z 3ZٿmrPY!W&g+5Ξ-yZ],|#e^/DB*:n2ȣF؆6G#k>:x?kkVQogخOOggSmBVXZ9JeH# fvt11LLuC;ZVh1sp`ZMc9OPn^Tu<͎23ih^[B"2R0GǭԙXz()nYdR/-h%\R/O{jF@E!D$֒I2Ai p.H1;M.J`cz<W :ϑ,Xݯ6ɱ;1!Pp5̒5^6H5;ܿttpDI`:~N ~&'@Z-ʞ}N1"ÓWSG'L<>Gt^&!y @B4<95izK{+gI~gl1><.CFvzkvp~:vy zD[60>*pt@)ϒT|O5'Orqi½ Zap/|V+z|¬~gg v/78>gG)Bv壝lP,9P!<́'2s3j +OGG,2'954Hk 8A"p t|XaO'U{ǽ<{;ûwU_k]}ZVݵko}V떶u}}mmW}+~nVZjw|ź}VYgs;}p{8׫ރ-t8VU~_jmZjUx8:_UWEmvڮۺ_ulvpqήsqs8pͶ(b8}7ⷶki) Y4&&q*V[feIy($XQLfBaưZRA K¤@%̔^PM $b֮+jbjɡQ0X{%P hUد5f&%<:rVUE-1]aAP(0h Sut(4h WbyA4~ov>ixmM|j:rDz!o/8;cOӹ0s+XknWL?K6N\ϧ?dv>Zwz7?i×/i>uçӠ2fp=QA(9JsNnΓN1>d'T?"&J{3;O?!Pcc6.)cpc-qp9)ni eeP%r-I]6Aquw<78?C8ˎ=O9JCMq8MK3Ӣqe|(a}3L'=Vc?xo>g f' _>y13.~'3\q@CqAE:8IT1Jg`RX&7Ȩ~LDT&Of"x<Ǘ2c&te~Mќy.&SK;[]0@UcJKG&S)/I#1)df ʘ<(~H}1FL)'OP9c<8cbm;@bbiV/Ï*sDJ~))0*feڗycqq kA4jgz(tA orq%kwNmWyuN}scSU4fHޝOқs? ;U>-ĎL #BC28q) }ݹ~e̪91e;Q7J9>,ePT  ‘' L" @q.xɆy?ȦJX>-R7-kϏ &)|՗0aM1˛c#) D3H9xO%sK #:KBoߣ{;qy8ܶ_sZZۇ:S'&Wq$S*39 [bKθRqNT D ?ϼp3w cw?f/6Јd]XsuDc#TDc ?9L>q0޴|aر<ߜe#bсw08Q5GoB<"3!ˏ~s4HrSt rw7;8~Mnz'Ofo|+fQx!)T!#7#-z'+DE+J+i Մ""E@JZ IZ0:/=@-aX2Z0d#PDyNJm>,x裲?FTm)p!Mq\sˌ$jڧnyz 'F̝N=p v_ӌ1@'c/c۷kXdHΟ@ecHiu *Aȼ'h-y]& R.wA^Yz_5rӒoTGQPI 5W704E=+_7iJ̨#F2lvE@0/5&ʬ?4G53Btd@nTLuDkBV=ɠNWH[6w3Tર,,hyM1q:ru,:J8p j4yЯΏĆ>pt*XIV& A\&>QsAyH((#^ 8(&*Qf< uS?kXQ5-@+Q@Eˡd֊{@&= ( %RQ#m5GXɰI|bCt3?Ψtg-U! j?~.W".ATgEL^'Aܚ:4}ViVbrA Tj NOggSoBVX[ Vt0l&U@Y3X]TrE{=c Zn1ǀN8:忚PʂzO!h(~v頭L}ԧ҂wU}.d Uv6b}Zϳ-9WIiZ_R j?>G2m(yz>}S'˦qGsw.ݹ'ogkU!I[/QvuLFSLNz!JOJ5~iMtNʔzpܯP=NBѬ̂TE-$4U]ֲLZ IsFʄHfbW׸B<z ӢmuW}W]^Z|6δG^=j,)wz3IߵO>h$g>Y]VL4mՈʠyDf^TC'0ژGgɎd2s9O[-ؾݨQl !"TL'qD't\ݻtO*n/Ϗ\|jKVDfJ$ mc* ݻۏXݠPO6@nM(2H-@ڦLm*.ez}u$Aw(4Uv#C>ZEYBDDQSDz A7PM1HH3&VT@e`P_Z@Du @ܖDY55 6cX"#b!Z>/råƙjHck0JrLPUd-q+%|%0ʼ G)}d9VxY'R$[|Yʴ*+Ux|^YWXdܸ C|<{rYWX]pBkD@w>6섏3|'ϐۄG 色⛉jdt'o^rב!|e<aJ |ψZ'}&ϑ0e*p ;:>«䧂E)7>y瞑 Xx CoF :cއ矋@0E<9G `CYW> :~s/jCȽAN3C=?>f)o|=O_g_g@px'C|oC|v7 W!AiEXN)^!> 'o^M NxJy:+=y3w<=S9Wi8{Ѩ$>S Ώ }!":" ^d;36ommo{m@my J)t'p"s =/A;ނ'uGHGGvKAmO U8fm香izԪD & j|3}7ͦ٬S]sLșT "h4!5* 7ͳ\*DgSb±* iiga)Ėfh0 lSYL<(@A>50A`Mh h>x} }>@ cx}=>x@h>>xҟx@;U_-iIu]^iiVpN`ŸsjGzм?Xw|k|H-,6Žt\ֻe)I 105-ݭBko]Lspm0Hϲ2JOg;= `-2efdջvU2~qrkBH֮m^;K\kYE؍Jْg.[k-T 6ζ]mJSGD*Uz%c֬e\eԗ\&/%g5Vi_-4|g^rjW+rm5+uI[Wfo4T6֕M*QSYWnfJ)4S%Wey^|fW6@ԌZs@*Ok;,&`66tmrr*xC))Vw + Ș4*w\]X-H ykW7kkBA' @'딕ڔD;rb'y]u˫B `/5=( LfH@j) ؉^I&2د_ct x1}1uYـ3s+s-[lR.eo]ZU5٠UW;/ uAW6g`+CX19/|cnB,3b21ow`fzϑgh'fwzԣYm(2,7JI>Q@͇Y!́L@ܙx4; Q̃91;N[ (~c˟i0Db-82{ҀM~^rW!L}T@IiH Sh1SGFhzR |tl\i,%MSM;vp[ ˲!'7ъid5TQCGRΖH1{u5FXY 8c0$$y Lix̀X Q/zfF L#qRw7PfK`}g& 0#<4lʰZjdG2Ock "LgfʇMlbŒmϸvaf<6{6:}[άx}2GY6fQS&GqH=iab#3u1E8㏟nx!!!̼ Ħ@R D9X%28_$RP-c:ٜj$:&$I( PW2h i`߹)7L.0>m3bv3NpȚ8lN[Z_,0o2I]nÝko ^ )B+ȿ{O;#w=kI%Q6'`hT $Q-B@y2שU ѢTfI02 <Ϟ)ĝ5.ArpP\>yJRI~x. 穂@W<':iIX0:P ˜iBժHr|qN)J$1EHWRx'N"yCG. uI i"( DxμaIչØ.<>Uќ2s>8*KmlkF |u::)P4wtɈ^]&S7M_A.i OX1#NiY/wa{{I4iuRoJʖ5Z^JJ;MIyj3=*UUѦFnIL븑oeg 4T+5Gyn)=:`T:R(jS#np%x3+irlYQ?hJ}LZIiJ&bGQ:HJs2آfkf0 ƕ$H2wn'.rZ];UZiʼ IwRC}(hpGxĵ(^8xb\bH#˹Gi!}X8~q5/9H7 xk6ܕ sO;8*#'gZW}OQZv ghp'K>hϝ&觃vk:{ >!> SW˳>hs ޒIyw.t NJr'?O0Sϔ>|ʽ 'Ltbz>'O 9G>^{| \8OggSpBVX\]H8:0y?&ֿnptx}Nx3W Ϡ"y8=R:}G8n0#@|B{<0mm}mm ΁;utNND'wM wqrކSV":Q[3ϴ#}9SP)N]U/Ҹt5{"D44lY^oiyU̽nm[LLSD]ex&oU5/*AH9KL*&T^ fkffiy5e&hVM\! 8RLVi2)Qr PXi2" Ykږ,3ChaxO>xn};x}xQ>(4v4}>h5xx}>x >x +~ Hݍ֏e~4Z? g3=3% #A۰ p2Οo?e,GO܇e~b>n?>kp~!z~Hyn^L?67zr XneMo8#Ls9@{4w~Lr;Ży߿#22GS,n ? ZSk~x?^3,,EG\ھ\b)CDDȟ1L*?kZ} &?p= '+϶gm(nmh" rbx8D$Ӊjy'r*^ kٶ4_~u?l>D;>݊;[9%=b51##ve;O~ƭ F̋Y<2_ePL}?OZvAmes-㠺/#5>'>W#2l8B(@q?,\nro88ssYc +,m̜@e2Y~D bxW7~D8/3fO]g[p_>}&cHumFY%D0oO_k.P hԈZmoSo,w|Bb$z=C CGlPܩ~ b?%P8~=3\(u^ y0lD4",JH#ݬ~~%bu;Q?@ 4A8?6#,Gj@cEHC@-Y1[J&|T(&?z=-)%tʶQ9ZKS"J A<=%1HȱJ ^v[bvy6V(26䈉 LD@D`s>PMuEJfi :r>3Q u9MTQUE>8J8\q3}ɳ zDZ!!=XXEHȄW֕Ws1)J2]VESDB#A088 EUF/W/A )sIËx jcOlԬS>8H_s]Bq^`A` P$pdɤM${OQ1I5֧D}]%*k'E'NʦgEiTFej32tT[*1v %(Թ_j΢ HUw*ի0z/G4@e^**iZ-^HXWW;IbB H>RAҀ DpOEdEؠyE5hDG#5+u>CCa3hC"E,cIu%*$I?WRK`SHIʴxtp2f$Gtm #XPq=Yg%W7eU ȌVdrNT3Gov'D'I:>4)0zI/<τyDyyP᩵&+ںH:pGUSxސDv}y=*5{@; BaOSmm}mm}mmmm@mtpΥw8/BQ;!@j j QG{qx8:|KR ۺEKGJ W0=9N my.͆6UM[4/!̉i5MT62D @ ,ʅTگٴ^.Q .e(zOr JBjLi2\E0ƂXhDS;9P!0A)2%"F8@Ѡ4׸s_x>>h}Gqqh x xxc@}>6ѣ?ƃh=d ? 1..qtt.\v% =c۞:}ߒ o`nc\|{nugy녳?2o&#-'H3??rRK=G(j/~0"Ic̼֞;\ȝ?oHG!臁H7ƙ៩ ?>Yǯ?}`1ǭռ)&De2=K/ܢOǸEv<ȸ?F΍Sx(|9DGϘ؉>1 ascZk,k od8@1qv7~OuH|\[C0 Yq$_@n#IF/!SQ`l391;E Tysˉ ܚ;}>OL1H6U4f3gXeb흛rbA bO/͐2&d,@i5 ;]φl^} gՠC$=F=a5rx ə"6[ρpȃxi*H6Gd~?2]lFB:HƐRϵ,؉F<|Wsеv:5]O\?67 6؁ݟ-E?k'3XC`pz[Ty8B捄:rͰ#bǽ0=@Y|[>T=zK P}_p=F) /Ϟ!#<琽_h'?l$1j)sc?=wR|LJbAd,ތ fm/J^|,iT21LOѦh;EAٟx=4 P6#<XG "HtTcߨ")=X]>(GuN\qHaY@RX$@:c3hqj%2Tc14y6qb[wI’ 8:0M8<g"D/ƒ+c< s) b0#M0;$U~v2 \o6؏+iW560XuV""z'ׂ() K!>8T=AWPLsդ,MAh î2i9+QoiWh>5VCFcY1%Qs^ @B1I+qS7S׎$Ƴ$DR N@z88ʌx$ I&2\cLZ#H.&#=$3WPKSri$"O{&J.W'^ᓪu p^Cd}jJ֌)G]nLAvR;Z'e֪EWPI7(9LS{\ I۰KkLrqGDj*`E&(R(^&EDzYVcO|*'*Ip9|0|[7*xÇO_>)E|Re$3#pxm}N<$d][ X7D:?g<<(|ϕ;?Ɉ|D'CvwӞ@u|}i4K8yhvÞ=9ix!;cJCAEz=sɏHQ/~g{H+ڽ8aڽ^tNފ$F{: 93&6YȹROB}ѣߐ3Oc~g7|sWzD;麟GH*ʟ#DsGeo\t9<·EJq+mC+PD8cz>ONwwwwhXUE`y_t}P`LCgj$QJaN4ė +KV6L9BIU3 E} h\pp;miyTԊM-[RUq4MT6붺3I[aĊLjU7St+Uf[2.28L蚞lX+TMFL×S cCS0ge a E (1T}x}>_}}}>}>>)?0߆4v=>h<8~eX_6-+f#k@ MZ76IrR|Kb32sn[*k>{|z_6+smvVWύ{bW )UZl [w ɽk.8#TM*J=\#b?6PM\&w~b1j$P7zZBg|Vݷ;8w5b#\K#T뫅9ke U;Ӯݮ i0jpK&Bv$(,D;k ۿ2$P3=NW[_5>QҰ⁨i 3خZ峈W\I峞=+j!sG_.*7ͪT6|J\ůn%zk9<) V˼uZrHwO`c/0~:;]ค. tOs'9 CRxf=!H:>(LBqgm2dCq[vλ6qO^dĩ>hBrd )$tH^:j|<(Q|:٧'B-Lu#2}T$ʘ>OԱm(l+㛜 r3kjgaY0  8^؂6SR17QPb bAcH"d֒ | 4FjA<>&B1pfB*EeV%/+%QVd U \PBA(pAx+CB U VbZl+}/v0u!pJCT%v8ϸ9f={SeJvv)/TՌJj7i۫*Y&(%[FV0mr5Y%r}&]2%@dPju: ɓB#bD:u"1DHcAIe^|r^4rcpO.cqç>+8&9opƞ}L<3_pIoxf4CcP<7#'!{f<8Z0:>x.tzz;[>9B15#ھx=͇x#y`? ш}=t'͸`ӣ+÷!JiU\:eS''sPCE|TxTXA!E@U' 1+(~ ~-= f?z}}u- 560Ra E<-d|pK5@cf#l͈E:8 a]6X4u&tv`HI.8IÙN$M&a?H)JA.$ OggStBVX^7Ge'BB\r.e9I! A%H$ =>Gހ> (>pt5x}}>Fpx ؀}P}n{ hZk@zߩFtSDL a`k}kIdˣ7ΦChC(={ l`1lvܯgFv"Ȫ/@cɾ- '@ucQYS\؍?0rzvJ?~G)󗟎w1'/?f܌9I[bzh ."YHy/`8u\?j-a-Et~_3$)LPGfa^vAag7V6O_bb}}o>>\>}mrDh2witD+VBw<\'rE-nw"0X#RH\$͐eeo2> 0lm6OK(>!,~%!%K=_oR}f0?|T>l> 'gK 'xl>_'^v͑X/.B]I$!WU={-#"LRuf٥r&[ZSZkMnq&}fb_0Q2RS33f %{.RϺlbJ;oy}}o MTQPp?^> s◉b2kKFx3LO-#|9O|'do/N ῆd<x;~0G{ U2LQs1G{M"VoVwDη + |쇭g ~'' @_psIS>ڷc @>o^_0uXx't~=>d{a#3p|%xSOG~Mhwzi'7̦;>҉>~ jggg-cI*0 Bw(]F 8ĝw:t/QD=P?{ئ|W,w:Ƿ{6u1QՅ9>#i/V1yoj}ITt<=?c\ߺRik.\\w忾;Ng4]&Sݴ'dN;::RVT&m'TO~y =2{U;&>N̿RsSc:0[ L$.FX̂qrČca>#)huriG*Q>ȓl9ۤB'i8ܬΦJ]cv՝6]̲#RFg,뜈mOT$#.xXI.J&/{F]?CiHd5;}])N#. nI\˖iǙih9~k#Z|)ʸDNW#@ާ)w<D͋*XKȕV#Eq`+,-6x8aZ۞Gf^G\ 'qu Tq.8X.,#WKD\ҡ8m3(Xn8[3OM]jY8B,g+9Ťؚg1l<ΫIcW%slR3)LʗS|/WU5<$<%0py߇>O{x@s^>\8k~H9xk^brNxf8WI|/~bt~NN=qȶL`in7lydF*:+f|9s '3~)+W:<\i-7{K=s&qx:eS* 199G}õ>o/ʩ'e<ŧgGg߈FtJDv|DLCs|`$iR*T`-+T֨(ICA r6bA#jtҴRPb  @ H@QIDA%H$y~>ph}G}x}G9{ʏ YȌe$@ 9."Sy eFSMpaGlx}87P?0DmAHP9셖{u!D(?NQ?6Gal( `nGG~!FR''HC R oɟ>m9>6G8..oo1kAP& CЏ{f" :և:PG=`:.(/&P(('-ae\mlYR6 ٛ7oH)ȝv/( pI2`&"}6~T}$~!2X3:>cC;3.7Ս}F?_h^xܙun<KnS$_0loOggSwBVX_jx7y{b?'7yD?Qg(ģx%'8AE4AOPG#8PP7nSF|H}s5u0Q5;,ϱ`qR#53ѧ -+\up$m߇+9Y[xy&$x@[$L;9K癓~ /<v||o/74/س<r8(R S`O Ǥ߇:v|룩$$ üzcyM{pPUkX<g+ >k^) DCuS{ @h$v*+}Jhp,5A_A(QD `Ƈd"cf BсH$ ID 2$"I I! d@$k_x>x}>x_}>x}G x{qcW݃`mh543;ֻV1f_D??<:oLPcOs;z?d#,]f/30Dy1C Y1 _vu3  ?wJ̞&E?'pbdo+,O06pO'"Lo?6fsne2z<&(7&z?Eߤ坢/὏gm_hAߞh-A )o?{o۟xfa$\>%rn/b0L@ʭg1B)#>Wh@E@E`Njdq z#9i'O)st5HLH%dRBrMH9s L9IIYvyhQpV,L).p姱GWTtIu4 i͘evϣ~K9z44NդNxޏ8)eؙ%7˙%' a6+&W 캒cmR)°+ P|k`y%-JTŒ1g S,c:!KĔa8cV;9gs'uNҞC8¾?UA4&+^lx}QL~5۱#U0vk "LX].BG5?O6}G<۟Qׂ  m9#FdGgO7_-W?#1k9~dm#v'(3yaN%}~D$8~O9og_m?>yɇ;S?ZF"Ce"{t_DĊq9egk1ܲdO'fh-!lsef_FF  ~~Q(>q6# Xk-`)ef@y?'x'q؅!Y+QS`ԲB%?iФ FPy1RGw̞`~kG8N8nKז_\\ԉ*S?C_nKke;Y졟V%?X]m>&WAy} ^4A90f~KZjo:״ʄrwA1ș"KvWGǿoPX~E>c?.QHw=RqO9(91Me+^SqcBIߚbIXwǣ'5n6Iۇ? '٧>Ƣ| !^I:u 2#^^bpIZWU&~0:S)y4rvFY `so>go C7ZP{3#SR矉/o'iOgyܞDÇۤ>fgL*<7O9톄=/OggSyBVX` {OA,'N ~g=|NfA ~D;4u=@HM:!2!<{ ;-ʽ'BX~|Pvԋd18.ul59l˄f{绛+gDf&jJ&Jn-WSZ]DjKJUW J H@Q%A$DIAI~`h,'<ϼUԙ5L/TZ%ŤGW[*E}HvW U%fzghLҙJmT+o6M aM{~3e"kv7عXo)f(_m\)`1m,Gd'W.R[ܭy_wmrouŮ _eekMyhۺ]MRg.[kWRzԫLZ쭼֮!yL&3nS/KI]+zyؤm}͕<7; v[gl -ɹ&N+bפv^rہg/ wPjG]O&NWHXsRl\Ѐ[H< y|#lUˤ7'j疥n\/:WO:?ź":$AѾVnGɸoĜU9SK2a߇HF rq滞yX/"adަE &l2ggz⨲*wYru&c @RrfWN<U!SL&27Թ342 C+Or22iN|w//<{>-L:~y|Z tN L1&s_'9èN`ǦVԃc^q9yXP#tk/xz(Y jhfGH^H&{Wڞp)ȟ' >^|Ǣ ̯'F}r|yȞx8hp*R@>gff?㷣I0G aDmNçgʞOOi e#(4х~b& HzNϠm.4;< mmsj . У<BPm@BPt : PPH A0PYzJBfPD@EA " $>x8 G>?1gF5h#iֆ&$0#ɜyo Lgmͷ cmi,~X?f8/0)'}?bHǡ>1"6r~4wHdlq󧢩!Z%MGj HYH']j3Ual0-L Mʧi*IKh;j/#dزmc W=ﯷIY 4;AQ,6D.\66=jވM ,7X-&Hozۇر+$+UO V6ed..c{ђK2_<]&uJc˵L^5G*Rۢ{ήX]mKh %wp`ƶRJU5Y8pW+ S|oĝ1^,BH"⎈MwG^N頎#OKl}NmIL,O.>,N}r y|^3iy|r}gБz>|V>N9!'/&CM8  A;4pG^9) vyx= S_C`c6V ToL)𚗕/.$@r˽qs=t<zr|`Z|2"+eH0G4s1;u^:9K|H1sg'gGO''%!85O-+z4NΎuċ9(<'4 ^1)$Q9~/ j+D&}qX=4%^i4PNHrw~6ߒ^v34=y7~u<^JÑR,DB)@wsDpqX. gi5iC"IJzv1]8{C5_2*؜Wu;bGz:~OUxMͱDh _{9O1y9ɴN竟ď?C͋̔X1Y<D=~?N}'Mw/s4w?yPi'cl6;`Zu8ʡsrdK`!'K(*<"p>y8)L9qo"UF6iJ|T0f"fp0S1LU)(n劂qO67 |SⲘOb?yLlg9::h渌 K{i)!$)RB)F*&XtیaG 0q$e?J;Yg l )ۢ/>Rp;L)TĊ~aL7iˤn)p0ԞOggSzBVXa3{6^(S}n?fhڝp0㒨9L,-؀A[ e!tŸ1>h-?sNgS3531I0S6qϘ 3ݦ_?~LN}fWk0dhvˌbL)ϙF/\-6 8|e%*&bs #zfPfrb ac*pdP˜f<9&I`f(ޠS(R39pSqڤ1{ S8N<t[Iv}‘2"x\NPI\1{aS'^@d"fuPSFi ΦS;&B>ӛ]jGgq@' Ps=` ItoM7Ӱĩs#y 9hߝh~ڜ1:9|lwr2_ 4ӛd!BCs;fjfJjKϦ%sJ,n^bgߎM9pPc\Bx:I}9F9lgzl6 6q;ӣs3?GF!v6>{JX~b2o⋂|>ir ?L9#w>q)cNMؓMrBN9wm߷ OyLɸ]G;q$)Of<8;pɂN3Wg3}ˆ&y3B0s}|f|hCnA.qF5}SK|>m6SE&}?AVlQyfɷc3]4{Im߿?^{ުΤa`[9~I붝J~f%OAm)?vr=d3SgXYbHqT͖-U.ڸ*)dȔ]⭭kGq A4&eޛJG~`5:=\FsEKJ\d`H#tqI㢴y_υxOX>e`p+lꕟۮJ,4yscS3 i4΁OMIՔզ]NӼ+Uw}iFdɦUPm2ւDDj&޻mgoOT|vY$I%s{8Kϙ(-S"%R:)Գ‘ȁ9*Up6YM'$I: di$b=[t̝mUDBUR\k]*$"Jp1WX]c<YCgњEVSfX%.3C 0WufAfpfe[:6#Ɏ՘u#{8߻cirJQ_)Jݨ`'X`ųZ*:ѮuWH1}>*ӤrQ-IsԜa'JOHHIZ{'>]*O+ ;l*ʓ3o)QRw~W;?Pn8'xXHI$Jf\ \ǻt˥=$2(ԭ;sTRp,dRT&QIaQ)IUl 颺}E?irNs/U"x4&U_"޺w䏵y u}m][ݤ}k9[=W܇wd3cǗI`p?s|Vql.FqI]b>*FT<'5g7T< Jr\Xp/R?"T!TUஆr׳NsG{lCN œx\gChvދ4O<<ǃ8r| + *PQ9}<)츜x?}&TVHci5ݺm 헽ٞcmmzT-GeNŴ&mӫmlw6Cn hvwH U(\_EIR((IQ@!EAr>#SI_$c~1'_td@{1YRR*iuH KW)¯V֪ow"rϫ=ev3M_m4W\R^+v^'*EgWs2daۖ)2'—lw ׍s~mq 򅿒%^pE'N5IJ(MqRnr`g-v!R/OY[ZB 7r5z!e.FAz];ԅ*C)6[r}nl,{1iL@ι+n#` %-+B`udFdm+]Ē%Vb6\RS&'˭tfJYY5ts[jk[e|vIYܫ.[]p^2ܭ}.)ISkKI9I'))d㖑? A@AK%ؕ s)qgrbulL\EjUdhwڽui.f sV.gnUqqR-%Oykaѓ'V du6i[lk%k䚲sW8P[xLuk;4q-ev ?-l1xzM%$SҒ2U #ЙU~ȆZ59vN:-&S=(|vvi~N?m[9pKCTFMr:Cw .v*] [~R*æǎP%0A{r J~.aޛJAjli?IdX#+tW2OggS}BVXbe,~ծblY8HXI@uv8b]J/-z > B7h)m.x\&R+%ۑIVZR{2= iҾ'~lDzkm՛ӥžk, ?IF&]o~嫷ky3o?q{2GCq, wN#f[/&\okRY%~fqPDknyEZpDVA$VNW%")rLU?Uj잒8uK'H@֏EI4P+R磧9H4 Tyszө`='mS*W1qAN|Bh!}xϢ^ 4B#2prqK1"_4s qac;5ucK$&Xwsd2 a.8=!;##qTШOJdFPa -YI=iW\ㅮƽ)a>LSi੍jX08Y\bfS 5N:7X0ɿk nR1afXښ*Z&QM՝uLiL粀cp1YAnEIŽ|D}̪c1ΗBR3,F5sZtΟX}4r*TԻCXQ}KΔE"&*O:8!J[Q>eҊ*]vwy ~[$9 Z)i(kAԆu!(Xd\1]7v|W9_Sr;p&M|98#޹<㖵?2!M'uuS?n8w>_H GB^V$]g!wܥy%p9 ,D}x( >x}6KZ{_3D~e;+Mvb-$r纮`6ggb>lڿ.Wbk*[ut~5^˲]]myJ٪#tvؤJŬV"ǐ2LkrE#ܚ](NKTǻogvԋUro˯|ޮ_ 0`6~lB}W.]YPkEsKS%Ί]ūԙ')K_ ebq jU WJ[:Z韇m˯${ITv- !ujzogfI6k˥9 YL\gWgWW v:Ej6+8ʗekqT뒧w\B/+.ׯmugg Ŕ-*ܻ]ۺͩ3kI @C8ݸv][. ȭ;.'gh)_9)9E |@GCǬk/߇q8,$QȦ>w#E~659ZQvKeYL2mD_7^v+e|,P5UR ',nԄ(AeΆ !YrȎ"G}ݭ˶za4U1t}={Ec,  )%KOS&sĖF7vcU4Aw_+D.&4PxvyD.~Y2qpZ3ʐ`[<L*уۛ)"M-O&Rcz*ȭY X؎F34u qv5sXbuRF!jI:hUVuźamˇV2EqUYl LgS޼'~0cA' JK1l+)s+0S~ᖋ>i0::s'2/?W~NkW^p(#3ɻ9cw{u)o X9xNX:wYûn:u=(So]CCTǛE#{!nx>=QȀ"$YUY'SɠaoiyC:({2$&h2 ]}0*{S¾o|Kg 9GiрӷT7_}:{>GDz{=F0:ڝ< Yb1%jL1G%!1*-z=݁cM9 ww;8@2Ov8;.cMQCh:n-:JI-k8u P I!@"w$@AA A R@ Q $DIA{_p+/|Gxx}>Ǹ;kړW\fXܶ˃ ծYɆ˙:̙ Q@{:&jϫRrٰA2߫j1U]ek׹InԽַZmvn 5\ ڸZu#_6\UzTۮӖǶ|ªЬ9<􌞫I_Vv^VbRv6iƮ*닩Iv.۷;kWXή\m%ieqK.ۜ"rB []qm;=z*fTkz&Lʜ2Kb7QHŢl *#C;(kݭ/skjɅR.ڽGی^:}_PJ}-&|W̖bpVYvngGvH=\|S5\|R^Vsv7R9dG'|DY#B3Q̏_B-3sY+y>}|ϴ:BYs5;9'3Cp wIpnr{C1 22ʦg$`Pv,Il[Z0F$Gz,L49_KX+leN?="zƵL1nY8kaRpN}g;%ߒVS R6uCT_,S9Rqɛewg-q%Ò>JcMzDMLEVC۷oHW#֥,<`;n}A"'x&\@jZ:XOySVjxyAi N@'k~@P쪨thT6#acEg53~,g4S f(0ʝXKqԩg&ǰ0bXkb%TF $`$e~+Xgַ'/ѥ32 UJ2\I@@k&HoRͯStʞ?xJBJNx)YK\p&M7Iry==||#Q$^1O?8׏rCoa ᕗz@:yl>N 8a/{@ D11)/E88a;BmrL;<0O{!P;}Co| ھv/Y3p&'O_vϼ0?D-~OOgv[gH/O'vkOggSBVXct& U }| =aL'{0FDŸ|M e,M>G֮b[mD#).QM\1j157$2̀tt`]56swr8ܺlqlYRPeAPH@!Z HQ"H!" $H"-}>x(t'N\>Ofҋ괩ǭWԏgVI'g Ob3e^ikD`JW6r&W&Ʃb%jv!U0 YII b=τ)?Q 6zڰrLV WgKrڷ]qBvrPAiNrT 9D6Ԑu]ֱ%\,)I=n$L̶K0|d˙S[Mk QmH:-k$pθ"[}umWle.uZ)1s)Erԅ/{9i(ÈTfRWZn#;n#S/j^ˁ!ԝfD2L[4P {7v&bjB Zl҃jv Q%rZCNbخUEirշ&`1K%̔(K83kĢUĊ BV]mJ!*n#79KյR1^Tڭ6rwο- r+uµb-"O?ځu,?S=weyv>~6dIhSfNʴ5ӷ<}etpvid>)qmFkg/oY޽{vjw}s2YR[OoT]5AקS'X5[8#nx%eVBǨ}#Y'4. Hs$ ?DPG. e˗m9[%\~icDC'_w d{e0.dw4צRU8wbt{oN$rMi,7ޞ_O{߾,ѐsco?E3GQ[VHlz֍EjV,3v6<`LLphbc;2q0 qd16u;#l,yՎ97A[V=%e m\*0Zx0iTx]Nnb1&&O. K =[,^0 &}X1cnr%F+VQ:N&}a l& >;$%,+8Lg[XZ PejհjYWگgFsT?g񫦵F2tf-eǐe<7Pp~93㞽2V|//3Yc'=&{}8=x ?f{S[>S@s'tzM&:P;cx}x}}=Ǹ|_HZ z$u?YqjW{j*yȦ.,s[-v4@?xg53T9縵gB1Pm*w>eZGؚv{㑭ZW{]7JW[<W:e-YJF}_?XGrU'ݯ>gl3+ej|n ʖ>"sl0bu`w;W\]-VY֬g d&\5ɒH‘uW3R7櫭[J0']mJ޷vvb=ZUeΙidK0gɤ^kWQ9͙ݚZs`lf`խY-I[b7_ͫ帹sYhBmsm3ۋ(!]|گ&Hkyk4ܤ]P9rjߌkrDZuG~FF[渶+;Ak]B ]p9u](*鐫_ʹe~ڠ]v z碘Peʕ-::c=\*#9e_i~G|D.wyMX6"{v g8CYC%f;1g0tǺd:Ò˩ bfːsyԸuU:hƝΊ9tL)8s2d(BY؝Q@ ![{!Ƹmia(K a>1LVt9f*='e?!"JUɚe˥;>`Ӳd=ah!yR$d-~gɃ.twT|UWXm+ M`<)qG[t蜘쉜=R]ժ8z«5,$˕[/ߜ>ʟjww{jR沜B ѹ,mv& D!`%)(UZȺ&Qs8a: sX+X,) ' ͉Jc8-$.9NJ5XW(w[XQ:(#J1Y21Y`걷 XUbG :-#$J,2V8Bj^83`~#D{cxPaƧ2,i#M;FV~ÚL-mL3flIq|$B_ܮћ>5ڨ*cy1S[i䓻[;dOggSBVXdRs0Oc{w{߽}_~s{zwzwg޾~{}Z ٯu~b`M|wxpgf07sFo ]Zc Pz0Գdي\Kwd%0Œ\-;U${%<'y[w&O|焀{߫R@]]=5}齇q:9J} kӽ a{+߾{OikeW{ZW. t=xky}U}^H$JR^{|7E_tϮq=&,}]FK.`٦>} @ = 3nfyC߽ߗ~kkx{_xD>}>>x(}s_xO}xx}>>xx}}xǑ5TMY:r% jXpD2XQy|G>-2x}>x}>}}t{} _c{<+(|ݻvڴQ1=ļ5Gn^.Lhc=+n=ݎܼ/`+|[lԾ֖~_ 4owOv4K͍knOa~N4R~v+v]tj5G=̮6C V}@x^g~!EXƵ}ڮ+_% #\@s뿃s+t } w@z pQZ,1xP4@7f2x }f=x| W l`x ]px]tx<]tx<]wx]wx]wx]wx]wx]wx|W]zx<]wx]wx]wxgK:x.gK9~g8g۾6x.gˮ6x.g4xgo3Cpm+[ mnۆσm\7Lmm6۴8vmmmmmmmmm>`nۃޭmmmmmmmm'6mg;QW lޭKmǽ0`zy5j8znm00MmPϻ9m0@ݮ0a6vpmg vpmg vpmg vpln6x7b;]7y;|!=Z"=xYi`P mMSI^*|=!^_kP. ~(g`6SC>!GfS \x< .#[Y" yBT tC-Mi? mdT+&:bMi0 :̒T<FgZiqq0ueD0>^saPc^0j Tr'ƻ2Ńn-t0K~Ko/k|C]1 ULBpGɼzQծUj*!.3K|ǽ[䳆, m1<~أAdK+y40N'^/_;=m;3"aȮWC > ^Q"ؐcg>-h҇nz|H=Y3< <^OQ __../`WK$ 9<1?OoyTi =e< #-ą9غr^TApϔo [ Q.6: WԛVm^$?'j># >L0 =ehvp6۷8'ыiN&CD4:߉c[`>OscIw02j%xr iz ٬'sM/C{x /;TS;__!0s U. >v?STiJɻ|7qB^CLuD`%xgN|mm! %7@j{(m>``n[~66vm ߻*\*3d"X.nl@v]m ?QkK[ԃcZh;MFokv%5ZGHmb[T-';xKjú1]>@ωt^oƭ~K{a[C'6ݭ4;[ۋ[g@Ϸl4(w1"O7UQ&o{ |x ;(Xə xi4#o߁QCxs&7|I>#*S@PM }wăx@zLJ xtn+cDġN1 Dj7 7d @Fyanv`0^k3.'߉60=xĕx$xIg+ĝSY4Vh^:xwHoA*$G_?/|M|J' O3z,xy=?lj˂p xF9 Q(x߈//Q8|H<9swxω` ``5xqbc6枒d,xO |Ox< 1>OCB;43|KXq!ycmj|O%-ǓzR~O(Gnנ)@>`>G(l?0q`~qaM@}h_`1;8߲m< /*x|4כ91FrMV[3*}< b+x߲Dx˾쁞`3gekw }tf&g'5<9*Yh/f+M? j(߰̇{l{Ȑ#{ ~"-ڇOggSBVXe?' ŌmڗG&сl"Mܰn===`X{zz3mad+p}FZհ =k(c/Mbղ?spUS݈={zy 輞O%ݧ<@zy<O 3JI@{&`36Ƀo0s}^8v?eIa7aai=a{+}{akd}{/do<ٷ{ ={/K}a,V/dZ<ݧ{`a4A콓d}5jA=MA?d m=V#z4$JR _վ%ݾ%K]Ŀ_}%a2e3[fx3-4vOeyoo(e&}&v7o8qm8m8}N6|N63'+|N>'|N7`'' }h/ooS&|M&|Mݷll}ki}/oo&[|L&[|L$x-1x0_zA7`Ŀ_Ļվ%xqآ:#?Ao?|<7z0xQ@ 0 +?G_E%FPLo'#pLz1${k8>(ǀw>6tO:杮2Ms.)2x3>NW߿{vٸAx}Bo-@€(L[9>ޏ`B0(@*%G< ה_B!xji@25E; w ƥq*!9d@2.xT2W~oP¡D~ f`LkO] }Pl`pxqmJS,x79SY: _&m4JҁuC3!e{igCO@RFBIT9-OtDXTC -T?p8\;qyP>%-:"2|9sQݰ&᭤ rGX,t{yQ>=^{ޝ+{Cwttv%{hQmz*/P݋շ#{w:Ww9;tzw;w,C+y)G: <>8#`f;L-6"^EbWJJzrwCbWJHyNJzOHF;:H ]9Na!{K[{b"!m@*ؽʼnIwýye=8L]\WK@B=/_%" 2XH(އpi` $]C{zwuwA:W,Xwe+P@PrAaAB IRkUD@P8-BQRgόS8ƊD0<(Pl( pY"3DD,Ue hQU2,f2#BJ^:b~|#ϟ"|#kZֳK+$$#LIYQģ48 WϨF$mO n/e]] tTi.3ҥX޼6*Q@ ]%411&+qhJFYVcLciVDG4i#YV:/igHOggSBVXf7+3Z UH Z|j46*Zэ$iՔHу#@xF#tڵB25pŜDoz<*xy+$S$ בVIY+1*UMjδӔ) jCGzfH iw'beI"uy>FZXLT b&MT5Rr'PJIRUJ*VTaR¥K ,*XTe -[TmRڥKj-[TmRڥKj-[TmRUKU.]BKj-[TmRڥKj-[TK ,*XTa +*VTYR**SB%JJ)*RTJ Щ9RjI]+wCޝ܉һrtt:WJwJt:W{wJ{Ӽ^ϔ(APfXҦޞֹs^C@`a@Okw'bT]/sl9C@:"*AU nm\*S%@wεƐ[I Ôl2#1 d3v ј'l:ͼΗԾ 5[Kѕ.|Pۻi@@dV;ƭoB%%\=G10G j ӊY('bt."xx8# P1j]Liۻ ,xI#i8Tَ.Ӕ`-kPBP?RT]Q&!4^_,u&uCd&MMrGWRgdOy㱧?N8VϚAtEkΪS6K(aMoPa(sJgxEF@8 +bA52*O>OSCP ߱9?0Tg9ܷ>9GPv9bU NMK I9,I/s0w,%,lѓ0N74q(tPն(?G ϘԽȽ'~@S: `Xw76~/kȰzINO^?CAŧ ظCr{*l9WA%Q{ FFD>%@#B  5"-* O&0%1T d{OD """@aI*xj*Nd6)fW<c L>#:C@iA?sǖ㤀 P j1CӠ*Qz+uGp|!utt9DSzTDzxxLوAA"u rRF߯A <L$xt#>@M7O@^ै^ڼuuu:gT"@.U?R~* &"#0* 2FDc.8wn9Ҩ5>'sٕY 쌗9*&T,h2d|MS1AuEEcŠN@&kkbb>=e{?RtQ]*ny+z)DP(z@(9ɾJsшyAYc)bRKKi)H_ɲ5qOp,O²bMFRR4XBmJ:nHod~h#iHV7-gCՄ^IէT -PySI 4i%ҥ>FJU3!PKuTn"|ϯ'TUyjk4H8pFT$M!:VV 䦜JЦi$$' D*$TJQ2E U**TVQbE*,TXQjEʋ*.T\QrEʋ*.T\QrEʋ*.T\QF *.T\QrEʋ*.T\QrE*,TXQZEj**TRQBE *&TLQ"ED/k:t= %o8N7tZ5kzZ3^5fn.J,'t^5pyVkHoޡkXaJ5${;7r脻2B߇`Rs Cz!\`z<~WD }+ܞt"ǽE6X\E=Y~ttteJΕoJńtIHmL Ye& F_fNLŶke:zXdZOY`*N^Xb/gE]6M Wh{oxt='Cz#\pxu1'e<VKz kdkZQwdFARlՅ+](@1*7mKToD"vk;ס8N qMa+-Eyvay$ߣ@7w$ fMj<-[ѩ~!p؇zH2{gGG_!zZdh`-pBծkh] t,(^eՃ Gj d< =߯_õ&<} zWYcİ+"FX4)L/AB~St|=ult`N=">mޅt&ﰈw{zVz,XX-Q`h`=%> Rm(ףǃ+`1>SN:zc&ivtNX'M-A#e=?FOcY Mnu׸|_[/k/oaluPC!T@Q 5D*$THQeD*(TNQBE U**TTQREj+TVQZEj+TVQZEj*TTQBE*(TPQBuE &TLQ"ED*#TFQD**T@uP 2yx裯1$8I8NI$$ӫJ Ejzs`8˝,ӇRݳ4b*ɼkZ5Gj>b ~kzmO:`X:;Μ4_`0ttDu^wPLF^;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;{;;;;;;;;;;;;;;;;;;;;;;;;;{;;;;;;;{;;;=o}TT;ԛ%Rn7Dȟ_Kc &J- s,dyKr2> gx [RHg@tV~@9O0L /<7[RYg"?s<~E3"[^"3It`NMTD7]<T2%+!N܌3g̿Ry wu&vKJJ51 j#ܨY۫^k$[vR;ߴaޏ\@j,_/2njnܟAb^? 3 Kpϐ2z,RfL%] 5˳_r@޷ߎ~Rnv~PγųF3Lda|ny伱 )՘yɳf}{,Ɏyf|"[3p!/Nxdui3_߆GN 'ó^9a]会$lӃ %p /c兌~?Z_%7&28q6.?qiX~<0#۠vd>v&,+k]O$coberHx0ZK#̜$1NHY##dGd0^R]fۄl3er\')prF&;f!nBF!? '&y- ψ/!FC!ͳs2W}oD 2)g2$i6|$!&"ĀtE wwzdgm_ꒋKie >oS]5i߿ 'dR.X\7[/e8,&czAnAlliTb eP=X|[ a||d9|^XuҶhhk϶5%yH.MͣnW-ՠHRPjzML i%ȯAS-TR^*Yʷj1@&}:mmZ]Q #g4j\V ːm\F@= =!$1Υ>Kq&[ѳeyJy 8QrTQE~'%̻^~\4r4vb؜6FNI H_!$Ӏ_F.\HDDC1t$*< V"w0%κ٪JOP$y7b@"IŒ=>sFU[˪T'W %n8|fx/uK*QW1!dxH'$OV&Ş9Hh Zq Ѽӽˏf(\|%~ fxEktIN*ѰCmC)xϗ۷+ zIYcf VŘZ ,b\n߲|#Ԯ{uV]vd޾sM젅 dF_]Yx&QԎ2Ps&E]x6s!\U^O me[ j)r;bY KO% Bk5r@ ǏA3z%x㛞12ѓVtu%GE5xu|k㵗/ sKX[aplս95tFN ]v9Muv3/,XT*X*YQ2eałPbP nbŋD[, q ŸbŋWUˈ$*VbŋAbžXh,X* BPaT*Y¥BP 2yo{+VLsTFtNX^po$'T9#%)n*ܯqP1<~…J@<+7p5>J!Vfθ44yg4~ޗۧeYb9rB|,=O!肦Yʕ2r<>cx=g=q69`h(U2H}懂)T柘9)٧> w3G\N+nCy/mpwpwxwxxxxyyyyyyyyyyyyyyyyzzyyyyyyyyyyyxxxxxxwxwpwpwl x2G|;g@|Aq(0񅁄d8}@ n_ODg7Jn¼qw2P([r{Fc*T-J9vL 8W}b8#4^&3\ۡ}}/X}E/Qwe,27PiFǼ|htG|>ѐGݜ@'IO;% ANፍq>WQɗ~XG<Oξ' WθM8 m{:8eXfXf`fhfhghgpgpgxgxgxhhhhhhiiiiiiiiiihhhhhhhhgxgpgpgpgpfpfhfhf`e]ZY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*Ud%VJY*UdU%VJY*UdU%VJY5;ґÓ(; R(I^'ٍ_2qḅkE)_`@ hMJ'@5-qRaP^"=##ۂV4PުoH2Bb6٪W;U)5u* a9= =/!ufdU%VJY*UdU%VJY*UdUY*vdU'eVJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UdU%VJY*UduÈ@azBp(u$9 V50X Ax,jO8a[X#۞paђu+pȝY%#V%Bw(PzBb ( .n --HM` V` < 1 Bo0@ Rn v . P"@ ,%;xP /p N> [H$  [ +$-8"!N mnۂ*qXFB)-KȂrW%sAfв#(4|Fu?׀OggSA BVXhRm"1Ïu^tQG91>lO6s'c蠊1 ш4 ߏ:#?R .uw\~h6"z/@Bld)#Nc{Cg386bB9y͌1<S>S>S>S>S>S>S>S>S>S>S>S>S>S>S>S T  ُD{} `{ps؞^ϭCaȡx8xL| D9}} _7`yOC;ZmDUwtA=mZVq봦嶩Hrd>hɱU%̚շbtmVͳ\ԗZk=|o3JM{vjN]eYpQ U0#xG\\^y޶ek7 N\ [D-9_Q|]]՗vԍu|MWUv'ҫUrImzjk52Խo_RDZgԚfwK/#l:Z;[˶ZUwr\+ir"g` +oN$ȯG3d[[%mq({-&\sm6y)m"3]sb65uZ䷴{e͵:Jp ?.n*AYΦ-֗gV*Ԗ]K~@bk2l8Z3+PrQ huscp:/1D}~Dܚ=,%s?bfv2JI7K&gaseb靽z)m.=s9o*2+UMʉ "NsUW3)v3#ɓ/}W>Iwɬ]˳|䬚q$WdI)33GOy3#]%Y֫KUL%˯#&=N?{Ih> ^)Ԙ9$p/+n>( uL0+Xx|u),UHWX tc{18gK $Ya)X 3YVHƍF8X\F]gdcgß|bc:yc4q@mtp&Ms|98~9c jx_ cN~ xO}$ 49Ifo~|fCmq79✅Gsz}c 뢬wl|oɩ<O  +'dti)) H'Wztް{Wj| r/~]>*^N |l:>O_oKρ=ؔ=OSO} )N4GJIS pz!}ƨqQ!ч@g%~8wTSʄZJ!k26;ˑn[V|U.]%i]T2EM8J~9΂m)w;q䊪AIBH PD$$H$ DPH)@$IH@}}k~};x}ǺƎ5_x}}hֵ88'*nR3"R b5҇^ZKU$F.W]2KKR!Rk̀r+M26I*[=Jd#;Z1Uڶfu<-e5p0|ba_smB m vL;=ڳ Y$iPbݲɮΘD݋(e3k|˥KI wyXWl^QD{ʢ.Ug%g(W30LzC:b/UfHio9a6pwW+j}U3.ͦfpޯqRjʕVT5:׹q_ݟœ'3ggfu&F&kkfL&F' T'XPC;m8[/9zS\n3;jM͊&{_Pٲi`6<׵&v"ҹ \d`> >-9\ +ʒn^gLYLV"y_+U06[yͶ ڬ*UwvN1j̒T=/;8~&"h3O?9=`XϢ> O0 k/#y'Df h|;~ޜ9JϺ㺢w9lO(՛ޠ^k"ʾ8j,rom5-2K_47O~YixSk(˔M.(Is}mhe{ӻZ;6oww+\NS;+r >e낥a\RNtiv7r-N=Y[\{]tlkS}Pg}RS!-/Z<R\`eL;}RA+A[$ Lvڒv2@T^I=gjc#VsF% e 0 p`.XYێ"a,1V8ctČ4:VZSwz/?x18af1M lOՖjhU-[K j%PŃd!Xк1WXqC C=$0כE[n@Xz~n"9qA5uuupXJ9a0 }3/فt^7GuBBAr(HA+3Gm񌕩c c#0QZGw5#Tkv-w&PBBOXY P;9>3+U;L:/Cjq3߬=4`J%+zxsIFr2Cwԥ.ug{ߛO:zsS~7~9siÑM'!9L":;Y}aS[pM@p&Msχ',W':1zz1V>>~>Ta |߈z#y(6>"|F쮝AYáFN <;M$'!4 :j>OG:CfUnY%$ @" \6 ` U )Mr*H$E*Q+S.% 0G[`=Dt .8@$o}>p}(> _;P{v(x~ G~2OinTuuۚrֽϪBy_6^[ׯQuc̓8 ċnjˠ[͙Y؜!U=0tJgjrnd%f]t_.Sݭ w*p{k.ݛ6 ZT'm`PA~Eyo&zvy?\Wz-ֶx6viLyjvf|ΠVjDvݔJKl%tݵ|<:ٹnS=[a YpC'K.榹 Ϙf`|gew.VuJ[y\lWllA&z^yamRj6~Z[fks׻z覭]gm5vڽϕ46}kW-=&rHnd؏s̮[kL /+8ڄ6Umuu@*>snVfֺ"xu]s-"n1Rbfܴ,.b_;AH=)U%keo6@9{9n`cYo+ p xXqϛ\b|?F,(Vy+D/l=jg,:җF'\ߎO TgugmK8~"9dBٻR,˟5!{Wd>П(26e:7/<#%|2pj_|b6[GHHD,Ar3ET+ [K=g(s=Yc ` 01CXY7I)|p3T/,fqbgyB`1l\1K'z n 19U1Ğ︶tmz wuqQ8/9X;_R3pO@ ;DW>gBk?wjLA%k(Cbs3oAd/\*4>_K>.PLfgjX}`Ak^WTyvv`}i_N-Z-x^nRڈ8U"b!in8^%n]k+׵qHwbSZpL b1)ٳwjZ/_\I}BbwvD|iEYVg =d\_Ijl{%.P1]lJJٙkv3:ULJٵ^+ R_2[n ۮzmw%և|g*fg\WmwYڽwKa2,>fzBkV[*R!.qRy{>mnzgߗ62)/v%5˱F임#w>& tG}g9Kտp\掊68)v;Va@ґbz]ɖ|Ӹ.,{?Bdì.VY q!Ǝ^k"Sk9d >`p=kL78"_4Utq.3*T{?'F@c?c ɳ/=L59)bUH3'J #I;0guĘ1$C iy#[1ōpc3vLdЬ][IIHY%Ndb#,4>0cgsnNь9qc1Wq`ԕ 2ePoFdBYt c4i9ҝ1<灼dp?WzN-{cGo~~o/Y'O~]g'\t=[  g"z߄qavS}N@NO&1NsIzY9Cѝu>4u/4s?s=^kxQ>jx_o<+og0x;%+GcOCa_w|`s=qюA  P=t^S^ ka*/Ct&Xc>3 0"H NE:M*dF6iPAMG̀[6i)7`m;mwlƎD ]))8F`)I8r9p.\Jrr4RuJIÚBN\$!-IE%Y  4}} x_y>zxPg;Ǹ;k' ߂W"z*ˮ".]kkw4-gM&YmJY[͂[9NkVݻKB]y/6r'^ j^`RuĮq*')h[^ۂABR#^(L!du[뮸1 pZLk-}]5Rv[˗V֛+\wwR@[+c;-~6s"la%-F|s1OggSE BVXjs)er! U!]7]sMe]qm8j]Bnmcka͎|+UH{>!Vs6Ϯô^keI4`[yR) 4˵auh՝Im]˗|T޸.':Z;d2)5+Uο|@uM@e}?ir_iX`07Zɇ-7^Iԗn2۟^lڑ3Pzr5وN^ז:W]^vNY@*U-%8D=&?YB isryۗ]vk˜$QW>jFI֯rL?ZW-̚A~`e#.|KfjMz@hb}Q~q~辳Cvc+ηu?\)J:NK[1Z$Tuj#}:c3'7:f,l]g[Rc*f#OZ Ucz8} +/)+&sɩL%;W;R Rj.L8wGT~!*,W_!Ҳ* E!i,~--KAk)_N>VJzTv8%e5+M (I#d_II%m+ U$k9Uۛ&F/(ekqXXaceb05tsĭb cgZjUeQy-u'jF&XHF 8Ա#T;H>}$䚮 P. < GpKSIz_%{1mpMr{8aU,Jȵ͛à|АVkÉ98ǃ^$s9Hxp IOsxkP}w~'{#+_NyxSciãv('Nߛ~چ/Oy}ߞw04U*|{FxWmO7٤>O8YyySx~>q|NO6XS] ^|a^'D8'qyOǷ8+=_' x~iȟ'_ЖĞ mgyzڣ$aNj;<>3_ 6Cͻ󸧈=>:WeylE4K>A4g'!>;;;b}(r~?`;!y89wACQ+" XV9<)_~hJP!Qe9>3a<:kQ8p㷶6(P|o9Lč_&aT?q?LȜ~5Pgf2Bϥ~<Č4ᬑvy46c11,/gq4}q14g9=1l0k3lS6`tdM +8bib7{LRΑ> Sarem6"~|ykii?2;u!L:?\lsˊYҝW)S4.x|Cy$D23OaLee19r'9%:XMa9u1*_񘉺sd` )81MA8%sn<˃{:OsߓiO nӚ!xywΏ9;& Sݴ6?zw̝-FLn^M'a;΅0k4l>jq7O'bsA&Bg"a BgG;٘奫6K7lDK."Zٷ#L`;gyL$3f3Y-$ҘLmg/=JKoϋgӤ&NK0ӿ bbDw"~_n a$vz:^k|~53C?~7KvOJ>:~FA.>uFS*O{Ǽ~94}z < +T{BrH|̽p!9ܚZIk5]ChUȐT驔mԸw'gnMg+L)/9~7ݼx/$/Y_ryZp-ZqJp8K.\*Uj|^X֝vvZ|Χ/3_dQ}T3UEsSG&jJ #,S'O8%${*\*$ ג[vZgxԥS˟MKRd1N8.%*a\rmTd>UK'_:z_Xc~fڰzk]r/V8:ZϕqeT]<{u/8w;/{c_wU/[#\j(:gWjʽwwv]nܿ^FwOݼ>Smfgo~hQ'_<ݗsO%ԑFZDp}hE'nij/J 55J 2ILcƥ#%qǒ4q2n>vƢU~6̎(aj}o(|ιGӻ\~+ _%ΩskTYtu3lH&$G4d{ΔxtP&tҍ &#aPvJulOKSɭxJVHV=ƚv58 0=E)M0T2K.*ì.DمqqґZ"%S&_`q&+E 8am*lՕ%cO>r"\wTԐ+E#mS(Ug'& \ٵBOggSG BVXkgxAlﯚJw5L4 +mަce M?լ u'PVe }4֗fٲa̩K%[?lpLJrvfHE`?q+Mz1}d 8VQkn]S4uK;eWI֮uks; $pciruRԓ 1 E[&}U j~_^ 6"p"y?g]}qnQv&\Eµd`3XjÆ80j7HNz@ N][h%Z,j^(~P% 9L*J;m)Tɪ):Yү~~Gjy50CT  뜂Ja!!54sT%)@s²wV>sXګv*XϽ)yWwPL䎊S<:td4ǺaKR4?|ԒK $DyUB{C}0GNYP.,?x &Ar(RJB,1= {~N=N`"5L;Y5kgw ye4x;Xq;ڀ4&O52ELg%IfNj]$W1!Ug=ƃꅩvzB缠}ŒEp:dJ_zz~8"~Hɔ -X.|9'~3WH:.T<qE-W?)Q}hΏRfd!>}Y:瑙5D9mpsN:5OԺ\_ISTukrԊS!TdXU3D S%/)Jo) SґO?}Jٛgi5oD\[V{sz-*vԺm&ua3{S=UQ%滋s$-.N)Thw1 bpg@k]׆̤B$44&7q`[|bx:A$&CȏJI pg#NI;Ir$@g~I`0.^6kަNG%sNp?6s[=o}|l8UW b<^7/Dtg/P_ j9=?7t>2CT7v pTFq@4guAK M\e}L{4M;aPϰA @Px ҩ!rSr|QM=# a{>q;}m=3H;3;!P{D?O'gO/g>i}s7DՏB`Oo\u '4Go'ȨgJRA8oʾ} ?7#^ \<׺KIΈۚŘB߶,g>S[qb6 &O?~TEkڸs+u٤Үx Hqߢ $$ bI@&ijbQ$  Ew D1I{IʪV&pvOG?gt1f~v3!k.joX^qڑ2y$Pĭa5q^Z gf+PY?7Ւ5\ZRU$Zϙ5‹5fmpг{/\T]s m^sN]sA QP^М0rv ZA&(۝[1Tr`ܮ[0݉*Y.Y%*IuI {B}kev RP]\9pܗu9^WָDw {vk|7-ۙ. /0}DJArk5IY0<5,27|?$"ψGΉwh~ZD-Bi0 PI7xFY\DVxGbL̉=Z,gsɁ0-l >} TLr6~+ԈWrG7=~; OGQ Oj~#[Ī>i|Rs4^g /S;YrїgNyN ںG0BUdz3}d9,jɕB)L3\Ӳ"zռ뻺^glϴD|Rv}./hPYwqQ\s ~h}S_8+(OȮY`wT~NU'N8oz~Rvr'Hjڱ7⧻kD~qϹd'㱩+iTDHR2N A8_Mi+;]&rsyH{t?"ȒD-٣vllΧ;jO~VWsМwSJA%S 3{|VMڪWfjɔn?{{ӹ7 s'))3mC碢/BПͼg)Ι))ruJ bv^g9km)%.o?!]I}e9Gyjjۮ﮹އzkiPv8 z]᧸L$mx[i泒ӶB,1ꐺsXeX[5e9i՗CcIWTdgd.#ZKUH*&Y1uUN{H?f⟱OI*F8'}U{|Z9B&D@NM$Ie` jLڂ;Ytfn/c uJҮJˬ; s[~p>j|!%ae^ua5uuue]c`@`J-3u,j(E4_TV(}M'c87ZBM[HKmR3Zx$<<9[9~L!+u:!b28Պ Ֆ}E0Z'nrWK{o@Q"}-,\2sJ0t SZBNEGJ`SY*TrU;*4it՛Rw,UvzN?ʥL R?XqH=UMTcGt*TI*Rj~~} ɣV B,14pOFKTe9 *8QL~ٙt)[R#.t(!=3._Nߗ_s׭mo~5s<u'9D=+˳}٪mʫQ^j3 jVIԿI5o[&VDޕh=`^ /)uW @-=ZKdfѤZ.wxI/'*P, Þ u!j5&YaQ1p&MsϊXjdž_J%Ξx~x~m;s $Ϧ(Imi()M|p >CNߣoϼVLzA=)3(LL=tʓV(܅ҜCL)ЩSC_ {tnj | _ڽ&B5=bP |]S  txE Ny#|=!9{C2y="ӃOggSI BVXl  +@@_@Y'b1)+PM9A'ri|Xaz_Оa)V(a,X3$:K8w*I!h]pPv: l82B HB!"QJ H`I ϴ}>>>x}>>>/ ~^Y~HaIgcc|爉F %[>`7"\so狖3xAsms~ A y8H?2|#1%{6~? ̕߀`Qo1;lN约i3I9登?G&[)ݍmqM3%K6#~|9G7_h` \3%5AԱVX<5 {nD W^u/N~+z{-[_i6GX'GI!Vswun-zy6SQzޒd{]װ5$ r_t=nYmJmV{ EZRG*w﵋fNZ3I`1Ǧ h|wvG'jڶ6Ey**,V3Wxh8u=ƷoIj$ E9%eX<בb #kBܞmx|̬ =b`dzbʰaQbgO,| QYq|rVGA};/yUDyqŔ3ӓE{ X6?ķLu] "!Zũp$,[gÓg緗<<a<]u='Xt9㗌~ <'|+USop*Uq҆[PS)ώXU=hd ]¬Υ 5U=p9I{#4tys랶.yGOH dߢ=g1| '| O ~a^gJy^x4W8(y=_wns}=v'N_g$D!8{0xD{{y{ȊbY@$Dȵ\}͞>)}=|O"} ҖZ*ְTff0-fl,iEl` j6`#c@-`6c 0`Ʊb3V ?D2`H$`D"! 2 X1 %0dA, &}}>ϼ x}>8_{Gm{ ~9@՞ǝf:9|~sdJYFq$ =*ʿ/GGFN;o /?\ zIJPe!R ?|9AcP&^%NY\3sF%| ť\;o%~U"=?>J?oLJVS&11pS u$`\]BMKrn!+'YW_9,?cU6yS2B7/fd~sG6Q8Oϟ8~ן}8} Zy+/[(O`gUG2t"s}/" oYqߌr>D>L}R< Q9lotdy^9VNТEB!|lf>μ7ȋ, ^8J&d~`SKy%{I5u,O12i C٨"j`ihXF6e2r=bd#k{.OdWK6ۍ)c4`VgosܸKWl-O$nυ!bitY|xK"*lf`r2`RDg &Qlal /K[Y1VjȚ{A(f}XmEZԭDF{MTlnMF,,]~Hm!TWD!-֢ޥ-Dm2X' NoH퍒S!rdIwKMVZ$a9V $|/5d3~gH8z9y G\Iф-qa a:<~yύZT7=Μ!~?Czoi9wM24FUߑJ7s=_DƷg)O zYgռ ٷOp|'d^CV}/]r[ lbRVzu0Iwq߽P5YwM{SzUrRgm]]5^ȑ_Zt3x?gHSBتSM3gV܉ 7Xy5KV@@B[I`e.η]^yiB !mϬ&:SĖ'QOv^7a#fl؏WOL q%pqB|>RVNT:Nl9'*\)e,aS |ܯR^6#NR>eVkFgrkYۈN{HU+e,!WQU_ _چo<%wRpygCA ?~uJ(N,eW|ω| .-Ϸ+f#}olC2|4pLo.ؖN:._zA&kOWG,G5d_gum-r;Zj '|CݷHǞ/Oc|y_[U=WPٶOg˽8ϩVL)'].WyνuQrOR,:?G?0s ap+3-Ϫ[%Ļqtn Pq{*DN"2_G:}8Oît%X ~Ǯ 39yp4yW>W GrSOggSL BVXmLB$|q]جuO➳?IۀՇX<5;ze!T)ݰNV\)#ŋq}$߽pt4ծK; s O8t&/˖ޖRZ9Z*0&'Jc&SVT-9oH$THċ c 4A@IO3lWyLf>"68D`ca&u$b| y:a:}%0ņ ՚ˬu-&+qX qQG хX`t-캺yVQAjڷC&l+K R8I)هS?s2%_OsV: ċ7l]*ޥp(]rzwAo[ ,mp 0L*K*Mc^~,.rWKJĔsd}0q_r8ujek&[uHⷝFU)ھɴp洿[lc )&9m9ӻN<5kcnkW8ΒU^im<%&` }Y`#-{BBB 6,m* m{~ i05k5gR5!'H8+xLٓR"o̔;P>xUQҥT{=<㳩S#Q./j;" m<;;i\J?PU^籁|g}ڽgz4|U€M>%vvHy'YOO/oćg^Ton)iEtx|ά9Qx+4NLq-!,+|@L|ン4N @}UH"40IV"-42 h#Uh)jA" J!A "C$bA  (IH}G>zv}௼Pݾx>}]A{g)4v}Mz#l2uUmVE>O(rxs5ۤZ8+uj]ՙ1H;k8]٪]&ݹ6 q` B~e.]Wٛqȅ–բ˨c")hL[|mp7j˳6w)%gZ@ &F6g&DJkܻ-6fȯ&ΥSVsԊ)]׍wW>'% uR&n`ZS+f!AzK|uwIXAd*]نg2,2f\핉]J@~wo52kqMͼbUܧԜYL6~g{ ТgiYvv"Ru뢺+j船;㑧*\ w%~ُ;fz|ImnͰNUP \'ûpZ~bgZJuзZ%lPB%tEW dV@',kn^ڹZv_Y%wbUİ\eeyT[UZW} 7cm WhG 43˙"!?_B9s|; Ңs?yL=e.eDB\12FyumGx% -}:dNwIZs΄a2#Ȗz$'ar&?heK?d#u-"ʺ|q2.<~lqv5 VLp%L$Vt-ԣ5M݁$*$n*FMQj8'Č 8 K%Y!pOG-o\ s28rmtZ/ ĂAԈN84 2ZG5siⵕ/I}}9ɡ}-JuU6S?i%,-)uIɶZYkwZTާiJHN}!2eR޻ޜCvI#gIBh+5" WbdTNnԿJHNO4ssxJ):^oQG[ӪE#r.sN[<{؝٭WRCY9!9 ]SHsDOqY)p&Ms3Ǽ~~~&?22>2x~uHcںzϓ"S!oLHJ {#RʁM>vMQIhI)EWji8td_'_sTSE9)zFNx?Gg+9C/ >e4b{ Ǽ4O@@t+L:<~g臃?/8p'>(tC>'Ӑ>WNQ籉RL;~9R2s3klm :t16v%զ5ͩh&[qWf꩚8JtjU-MH) :$$@B@@AAp1(> VO}>~?ja$Z' Rk85wmp.Brm6*5{:M:w JV&wZ/DmHz\8,nqy0Diֽۚ7V /˗_ftR{^nL4֒um 1,\˪] lψ]6glFV[Eٶb=qUjHb'^E!n.s$M\ ,RgmP1.A2R6dR&l meWSn$^3x1keٝ;H!A.-D]k$kx~>|RR'--YY=iĂ>yqsJ|??re5z#XUH&rNo\Z,vXS$QYG8eT)ʫNCF3\ b!Ńe/~)'\CQURUFd*RAk?AgNzoOMLݫ~—38N e*mt9%|?hτm/|]"eH!=_"un8(X|ڳ; 3RXy1Z ^81F *qHd4i21,X ,PwWL-#r>6Nnĕ@>FߕMZ*sB.XCkBLѥ-$2jtyR2 oa -OggSN BVXnpOp~ 9'}W8~q>#?ƭ8ǃ''O ɠϰ<ʈÚh0$o\3Xydw`\EXM8;8>1п_/D!%}'LY0x>gOق}‡Q$b+d *p`@pӃGMsOTA Q"c&>xC >x ~v~93J,)"6+}b $kۿ-υFBϿ>I;rߩ*dIgbQ#w<~}8_|9? | yod2A=ob6۟Q̓7e1;% ZOr# 2J:K3"V09񝑟Hcq497QRD/0r#ʊ Gvp>9f|Vq1?^#"{?4"Jϑ#OM?~ ;"3g--= 8iƮy_>3O))֋(^Cg3:!#7|!MG9O/}4O*P5A<:)g¾ć&RHzt٢&ó^ے3@ܔ?N%D*dLun5܍RUP-ڳNgcl@VaawͲ&09ͮvn3uwi$ պ!$ ? H!AIP@P$ @$5_{tP}p53X}}%-uR =j h*R7#+'W nZZgW2\܋k_NR׭+){v  ZefjkW5pԀAN~>brUm6&hVn1 dλ C՝.Q+j)D e--\6p6\P6]n͝7qvy47 'HT~՝;.nq kR*k|.!ݼO$]QRxcZxZ*6fp:ϑkRٶ#Z}lYq·333@&7rY"ۛP,8`zm2ѳ:\r{j Sݹ =i[fqv\@}Pnqy-ɨ=l#AK̖hAmk0-5x@A^mu^6۳10} #1Fɉ?:˭!/33~os?OT\|+ 󤋙'\ukl24}K톿^m9Vlsq))v`;,x݋>U滯c!,^N;Ͽ[mZ#&VN.w8 yw'gc) ا-u._!H\Rc~:h89I.sZ(8Il~U)M='3?d!sQǯOmW'"G< TKu'oyτ^gU3_oj>f*vy~OM=~ x=Csz>Z{%8 9"6NO|ϩ5z 2')|vt{=r= vwp\ݵ6S&vF %(P*W FSCM)" ]ªP $@QL I$BE$BA$ $A) IHch>{_{}x+F}}GPz4};x!~}:򢺮}pF۫?WNյh-TsljkڲLr6lmܕ^]ڀMOT]ymخgvmox[B  eʯɑ=!u]kmjϳ$vΉs]Pn}/pB_:emZ^\&)3> nе\mImeu^Kߖ)b[uҟu%q7kkK)w5u#^\ڏHH'Hxjܯ"2 ƹ@ݠWbղezyTT3jܯms=Wv ##պmšngĸ2- "V]BϖڅYڒ0 {HRLLn+Iވ]xaE {uN9~wPڀ (at =K|c`-"|+tۤ>͏vf=Lv2Lԕ+ۗf['If 'S|aȳ,]sY"~:͹2]zO/@>ɄAD !6}Q;+ufno+ < _Uk'9BA:YgaS'(yY2)zXi?ϋ,tue-&u.\պyMR}V(sRY)Uܙs=dz:MNsX@溙D2dUoNx\:[V9ÝQ! 2rSk` 7̧ܞ [))~f>+3}TG\aOggSO BVXol*ؖHreVR(jq6]Jiƣ}Li4]-sմԑ\cHwP9bc=rx EO"wzȢ ~wINq<0|<).H|ZR12t0stpRrGYv}g#ЧSNH|bt382C :e1̧=O8:kvKs]vڍ\:Nu֨Z8ԉR";:nDK뮲ݺtQmF—wwvsB\vk$ ( ) ( AHEDDDq4W 4k}>~=1pm>Ij{ynurܮVxͳ+ulkJۊ [nע`+@6zr2eEHM_j%S ĺn .r6ro.I0B 6Wxp0oNeؘ\RnKAmh0bA.m9 !rIsͮO5S@>i( hvkkkuvQUiσeΜUnDau1_3Wvˠ\k2]n+@ToLRims;WDZnd PW̐A6\߭`u̯l\pNTy7v[R[0k&pBܙ wtv\f+`={MW.-%)-.\KT P{ygUr.j8s:Z|y>dYسx30ujQ{w1|Ԛ+|u.JϚkqOq:ys;,jE%zAJn{&E'ܸi{yu6s߶v;l梨תm)ɧ}~}mO7m1New}ߦ 6==VHP6#,Ȼ*X3}Ƴ8~=Y_GFOvp:ږg4O}e~fwm5N~h - ӧn:.@BCu&dT:&YܒR|Ӥu=@.4``'`OJ,T;(YZ#.x09㩝b³ǖ qXXGc8FL1sK B~\qrc>0۬F9㙀a]#hNz#\c"ϳS,aeV,.#0-ˉBq!cdaYXPZp`#17TK "/gJ۬N6Ikg:x%#2#f̌qƇ!$ev}0%pӘkI.Q~Oc|uSV>Lc9 {˻S=N#Xl0 *|_8<` XlFyp>l0 *|[<` XlFyp>l0 *|[0<` hl|P *|X< hlo|p*x<xx_AH[= >lmF8j@|< xlx: x?O&}}mǟm}mm6om6oo6Ͽ>{m6s{w/m7om}m}m8o}6ommm|m8om9mm{m6o\o7sm}mm6om6om6om6oo6ommmmm6mr6m۟6mϙcm_ommmۯ6om6ommmom6o}6om6n{|mr6VJm6om6om>om6m6m#mFmmcm6{mo|mۯ6mYŮ6{o>m6o}6m6my|6mmmm?+ kwmmmmyMmmohomcm}ϱy7mm}?򰽰mmmmmmmom6om6om6om=oy>om[mymm+ 4v7omm{FmmommummѶ#mFmmm}m}Fm6mkmommommmmo?}7oym6om6m}=ymv>6oomo}8om6ۼyy}}?6o}{mm7}ǟo|yxoovs_mm}moz]?gmmm6om_mm}>6{mF6mm6om۟ۍ#6ooymJ59.S<Z1, :Xbֿcqܑw6mym6ۗoy.㪞.Wp( ;jkN o?mmxm퍶6mޱ}6omw6 &<@,Y|Q6[EA0w\卶m}}m}smmmm;Qk.eZ8a.:Hq aEB\C/7Xo;w퍶m6}nmm|7m$mxoٹ$P#n `,&7h0 {mt6mmqnm6m,}mw/؇`H^mmxm܍m|ntmasomJLOT3@e(m9E4Xzچ|P 2hK ƅXTl&TR8D3ʽ e' kchEaцwDM ;y(3FxC>mpPOlJw*%VP2M 3#sF(gLxj,}TR "vtL#56~/ q3SLGM@r+぀?hpt2EtcßEseX.hD%3!Vs9ͦqAE KsǛ/ my— HV R @p*QL^QZ n4xX0h7H3@әh ;A/~/Yۀlp FH6Fc!`CUӄ%͇7jZblxT3 [ƬjDH \*.`U 2/ 9<m({e$z)8p`E.q=:] \'H`h:G@ Q1PmǠmyi `_DX>_9z)쐆T7o]_FyD s&,൑Fp_9ლ%c47o9W-<1τ'|"XfmnUABa#- 8}O]qsޓwwrE撧g LO$bqƚ:T" sZ"]:R\ ix)Q8c H 60fJ*SDga׌evXH&lF&̢4ȰPT zbq0ڊ a;wme3Pz͍Fv`f 8K<0c! }dq{J 77gpf8E0a1Ƭb6 @|8{^g۹llG`LJ C03,'klbm^xXJ*ׅA ^.B c#!Pc#*/W-L |f  KP%WK.x ({caY 襱$ }kiL`h%# u +;0t8c`,8u&`/c`/W6\Ŭ-'`4ZĕacXN_F1:*:x2>!fCХsK@&dhꍜ:~>X0)oLH& W? kEޔD`3mqh:yrN#TI5Bg83 m8D%cH?*.7qp0vc0vadJJufdO> 7KN 6곹1Y0va%"0@XÌe: Acanf9n=cmq>dѶlK4^ phTy[C ƞA#"` 82|8~60j5lKGIh{0t#h`atm_wJ、KQK8PPF~dKNnr_\ P!WOL ?|( _E- _E/xJG`H1 i QL~?ݗe/?)  0ɠswq)3̓a3 8lŕcb]8.~ ʨzؙKL&b$]<@2PI( +6BTF2`x[!`eߨ8>),| S0ˇ@c14zpdZ(a  O @ey_6t3)%dQ뫢!j(Awmm9N܍}E.42el + U~10d)ت- >p7܎@LXþwӶ %e>^plPU0z~V}/UHW%v`?g"B[NhT ]zl!!08~)sH K'MJ'x~ 6C+X҇|G)@:Ot[a.ʵ`ZaD :4;Y3b Q*`/|oWOj4N /#|{X3`p pf!VlN{L&h &Q4~J7iT om*#x5~^!`D2B̼?vG{5f9Myb\@acTHU ;܋3y82-0 KV b)n+o;aŬĹØ:fA҃#@ m{mm<y6oplVa(eݩⶢ6ţT8/ q mg<~'^0ELH*y> ~YD#`A/0G E?FA?u4 HV';XAO+TPE8tேp^d8xkdPx||`H@!' 5&zueBAjyg(KCuB1Y ADS0(c~d# $ociςH.M+m@ ߐ ~aa;c|7R.t~K,ymm6pVܶ8C7)p* $QVww:RbWCBDž0j|K /tNnΜ bXC~#UxB?a㙯 >mxz>ɦ~EG@pW=_-)FՈl4BCA?0d=c"꽺^UIA3uw; x˙𯟛+SP3A [`GcKFr_*g; W!S]b$efsA{kju;\-00Rp{f2a!"H?ŮAp ۧ`d> "˪W Տת<Ds 7?~lxvzHHbx@_<'{_[=TS2%T#ܐi|븠~֎6 g&WqHX37XϝTJ2U} H~S z+%mz#wmFSt5І[܏ "H`ݧ{h}V om>|~KJix gި#JeM(f:B>FBe;m/0 6  _@X(^pSڴ5æ]OrB  0 Kd[t?W/4WQrKǏtbwRohV-kuߎz7B@ e9JUB1ߞݞߚd7 ''!li%h(Y"K?Bsv7,z~Fmk m PPHG3>!=Y:a-Ƿ6kym཰m6om7m6oǻmmom{m}Ǜs6ۼmٹ|omuoͿm7yA{a{f6׏mmmxn|6omm6om6nqw6o}66om6omFm7mmmmmǛmmmmmmmmmymǛmmmm|m?}nmmmm6ow6ۼm7mǛm6om6o}6<{Ϳ7om6F#mw~6ۜmm}6omto7moKom7y6o{?6{?Z%om7m{ۧm6omom6oo?6{m6?6{oo6mKso>om6ﱷ/o6{mommmmm{|o7mm??o*p>oK| T+2|X<  oKl \+2|X<  oO|A\+2|x<   oKl0C\+2|X< ` oKl0C\+2|X< ` oKl0C\+2|X< ` oKl0C\+2|X< ` oKlA\+2|X<   o \+2|~<  o T+2|~<  oT+T-x<PxR`;xXAHimr 7|T+T-x@<,_݃rA` >o.[mmmmmmmmcm6om6om6om6om6om6om6omP}<e /R< n7ma[;08):wg{w[gsyy$3x|\>,~ ks|?>>>n$I@?w[w979MSs79Mcwﳹr>:-lj`` @w[wnSs79gsw9gsܦ3ϼ7;qo㰳G [[rsϷgss;)Ss\rw3` SܧC3Cmc>1;)? /u7;w81߯3Cwmn‘2{)P`xc>1߆6; gzgxZ3lp0P ʆz፶1߆6;WsޫwGux3zAg޻1T6ۯmMcw϶ GGaإ &gG &߀{~ƍCm] gz~:;krCl}N:w[mM mӟܦ)nr+4}U94noU79MSsw81m, @& ϶Ü{EG1w81n9Cm1߆6ۺWs3pc6 mnrrtto*dJ{nSsw;܏8J&j(*3Pݬpǫ1m;Z34m(cmG9ÍFؔ^B8) xC:yÉѣ}7 ` tr,] gkuء%/Չ # ՘kHɴ͜s(UPѶmmn Anjh} D) ÿPb:F@ C*!@ls}~i|?x.s*/4yp3+顖!(|#+j*U<\+o~ۡhm`/ƍU9$€,G2 F>ŀ BX x8U `x`|(q}[p~8e7&=/~o{6,K:3hCcμɞ}M ) W 5i0+&:3m<=lxٿr).  3XC6= E|!HЗg QkL 6|vj#@!F@ͣ!<g&: cvƭ~GuP27pS5>{)҃*˿'bc4N(xBT[cQK G:ixCp7sn$/QaK`_?"P>P . cOM> t]|(fap"L PͯRb`` 6 ͗Hi m|iJ (^A6/~3FpeQ#!b)u3jQ02 d TP`I6 3P `IQx{ov<(,~Ң 8 Pf>q`4 ?Etcq58~ ycghH$RS@bp!ۜ4qa<ʀgW#OX)u@ ʁ1rEr d+K xެ,~ < T%t0"GiPQ3|<7yx0*='nOTBrM~#qQ,V |"V[ʲp v:=o x>7?x3H1~}c69wG7p );ldfВ @Pd%@` 3N ̃Wd p/ /X0)<rC. S)d]VUsGWBwK,!` 4qX>,> aM/S+\+sœ V }t\r <u@/# p0T* />aWv)P04 3}| P6 p<hS<A(~%:cER@dD>  3Fp@>$  A3bN@?Ā做oDX AE3Ccx^+{o~ CĀ>2X>$q  Vʠ>V>yyp0 d<ÀSυ!pb(G(4 ɏ sܤ>Z| F?  '2!0C~ɀXYJATC o B<: gbX>4~0fcaLꂡ$Kd(frQpKAg0 ^5ǂǧ krf(J u@]|?ˌEP>?c6\uÀ,|?j$qx0f7{ׄ@ǚ iBbrka[#O FnT" @x?A`T^|,dch) t2~68b13X @DoBa@0D @Q`E QId tBMjS;(.d>j19|9_ ^dΐ(4WJ3U'>.0*3 (5`c|fg`Nw<3_$$r6paP v^UWm`0||T /0d<#چ:@ǀ#Uaw uBwq9?4Ӹ<1(:O~aMLGG &p)ψ1a!|yh\38q#BZ @PaCf%*uz<3 ft :EzRßfhC(m9 =IQijc"Y0f`>ꁌI΅ 0`CxV[= `j8O %`Ɯ<)xHrbP>?|10QA<' C|T 2:AM ` qp11H k4A2A{’UB(f0!^GEX}ί,h蜾5x 2\B2g./APp< lMy]}=Z6l]ESGG|ꇻ9Dє$|v">QU!c^?H( 1 gć3Bd3b:|΃0a;U 3<ڏFT#۬GL&E*Vxg8fdh]f2~WD/[ch@g?a & ӰeAgp *JMk8q?4bzD H:"Mpџ@)av(\3 a1 ZMpDž4˔l=5Ͳy-$ѥ 300?< < O;Kp&c6n!D̃ 3&eva*p bp-VQ(sfժzYӀ3&l2;Ff v@2 d%PeΜOkV^#lwԄ޸2ZN&ٓ5 DǗB1G 4!J2 A` im* 3Pdh^s; H\Q(@_*43ĐR@R0HU s^'+6 AK#C)le5S-Ldl,O4&W` c$i;\ڃBD>AFS?SzecBp0{DOß((l)c] 4)748is_x<}"AOu*>0:6>:4+S|8:)N;֙% $(#^![ɫ<H<3?ں|SMG=E~* 6>weIs R5QV8+rpdX>w^'̆IVHF;/JzK) ( \3?sw9}}785t @{;h mW6Xh1߯1>[ \ ꁋ-o/hU mpcm_s}cm㣺ttn((,w9}] m;6p㣺ttc2^ _tV\(5B79MSsۿ5.9nP Lt- fm7݆3c)4m m፶o4m)n781vzۖ܋tw[otC|?}l}nrS Sx5&81z6~ƍCmc6 mMSs781zma3s㵷5bp=A,K>!ϸ2}mp s䛜}npc9cwks;3h՝ cG1߆1oP~gs0gts[fa{gsǺw9gswG}] mmnަ&%`UrޠB _ p p7s;հZ)mnQ/o 3?M_8pc_:>,#Oɹ7 '8 1mF ᓋB5yрC_<᭓x6S@gQ<n+xSgpxUTeož cf]QM䕷Mf/hHq~V=\2Pg -- ]yp"x~F }F\#r,z:8zxD/>?,3>j uhKVc,G^ӡ=c`kCڡZ9pgw~ 2 BNna1Z ujsx% r$ΆNPڣi'CbR k<'5rw(T+g)+{͗s `8֜ ׫5p <>fG"g>x։ P_T`X|Oܮ=3LFzg̐KAkbaɫ2! `Kj )Bu/WM3'O9v?Y/g@!4TT?>R{ pˆ=OP]Y%j.;BuFˠgZh/@$DH,6nRRbV>m%c> q2jHDPMoE^}NSL2[ |/1B|_`>/cŀU xq2@c ӓl2-[IRt.c(OL(> yB:2ڠcN@oHkkwLhi }>sCӥb4!<|duk[Ӈ)uR}?Opz# (8H`dyySp w$w?C% c>aMIG4t1m6<Ɋ#UR1b 0g@XdYP> `')Gg0vs$̆^ҳ^RR3xhE z&̀@đB{%C8:N2iFB+A16>xaL5Rx >dmW70ɐ07:I4R! K! SQ@TD `[A !ʮ# ޕcynsGVSrJ3.VHډŭ$4ZWpO%(`sǎS|(ü1[ 3^>{ٸp-{q5G3q',G18cmpP-;l@D p1`  w-n-{13Y \ {f倴w)Fc_1Rq )g羙qnO]ʍ+ƾ9BzI +D@.—'+R|?^{b̀X1/X1?%S79@33$2#v,ޑnX>O}1| X 7T +50& $ )]PꮏNj/;%%O_њ]>FT!S׮q~^b _AUO<h?ӀN#D @O‰Pмnp'3FDc.6/?n+ o8gX  mq &I* P`<8fʉ eM1#ǰ R[NO Ճ raX2@4ȗIWp C4iTko: P MP2ʿ\ GWT &N{3 šA`C/aY 8OOfyeFyц* _j@1`|(ôF: gAfD?3^2,P$`i8BX0ec>$!pjx<,Q'Y˜c @;^s^)px0ij]%@d J>͗8 0% 0 "*cP }jcUPTp1=<2<$y/pMN& BOt݆VM_2pRؿ9y JT |<+:T *2T;@M xZA:'[Եsx+>$bt 5g $m(]22@ `h(T N?@|c OD5G̗B4c=28y4h‚'vB0O@DgzGLçx%;X%pῨx 0ahf 1@BK#J՚I cjdj _eϔRA/|1G4`@[A RAM >'˨4Mg6 J"cE҃p1`f1GU`йa>@#YH`^bdf cOzXjx\̆F B&l)M{T>SJ0PFX0?߆BwX#=ᕃB0ɀ#47 j(:2GN? PV B3@hp]cq{"J8>by x kk۞0 j).F% 44V ' GE_:\q8':7< XÀggO 4L+V 1 mKz#azc0BFE/z #AN@S@f %jUVz, GD@)ؘ~;Z rCcvK&?2 ->JyѸ>`ET10Hӿ C!)P3t+VZ0ہp2h//?V@ (WɅD@fบ5$PږO;t4ۇ<˂p17"H08}U31Hc/ $[aWdxD'_u8\2U``Hx\ Q 4A-XQXxsmx_M,#5<.׼g?%V2c"5*Ӻh0$ߟco엘iCGԱꄨ qUcJbB7ISkP=WњQ)\くxE%9 |)i㾰ɚOC% g*DIa p~9(Sƫ՚>GrNi(QA~V [aHgab h[7`)@0@NHVG,*"m@c (3 DqcrPc0 O*tMH.._cB>$8d{ fu1˫TS`FDwKaz+AG^%Έu ׊[{›aߒc2cJE.cƓl|K* (k@ `" 2{|b>/fTz  ec3 ^]8)X12HVK^2">t2M+Pd"?Ǟ0U Gba`1`P43fPmC ӱr;rNh:=ANxAA{@=J ~uc B{d/GD /|p]B@̥R E!!UšӮ2!p0_ pK83 yB+?<@(vV8O ~<G8lʰ !s018ۏFǃW):__:(?`!P oxfã2[#zp 8Axdz<[$AagHxfy ǎulh11G`O)@]< |S?e p68>RGz>vbH1G ob@̻<j\ 4蜌'`fq\1/^3S F^L0ʊW4q [[,@c`N\$` l^%V0׶$hi=ףL :{Вq 0A d<ӱeriჁ39]>BA6(:8dM=3z\䬎tuͅ6[!yඩSެ ׏ܾ=?~*IY{c:]rV^rI QBi̽iX/U _#{ϗx\/{a03um`]wf6>0Ow&x d1g(8@1sX.<G 6:m/{n|*J%3BYES`MgVqL3 R *4k [2B h3. V=5[}U9J Sw?A3ݟ|Pʃ"en_)Jl.4>sMۿE,%o w^,.~enmA[/0ڤHf?z޿cwUM]`Ť>b~o*^lAo5C C>XNt.d1Ac'[)|̀P6_{>/Ӧ8>)R$2>TPG# p@TA`ot zݜ) GRl2g#Xgi[$,sT9’p3,[@zp[Onlt0q0-ޯV1!x>JP> קH<U!FCoW8-/? 'УJٶBcbwPà}40CcÉ.WM*&V~{)_$$8 T@1"^62/6 q2#Pf0:'2B (CbcAA'H0COviD UF8^x*OWJNj#C @ǝQV?2$n /P g 9J/Ua pj 4?3|I<)P bJR{ĒJtx1Ȓ`SND WG J <A.3IVu@~=z?;ĎV$h1%חOp>謹(4 S,2$ : uD:?(˖D0|8s›X#|/6iacTQZvP3>'L@%~<=V R*Q#<(&PfC AN!E4#Npk F^og^p, g 슁WPH6, 8pfGc cŽ #RҼ6% P`Ya>'az< PC$J0e-tQNi8%/*R=w#v&~zaO΁ ŀgI8P %) P)/`BP 9AHi*]t[gj#V+"j|B^<)^ǗϨNyGV `JGQ Ohf"QG< dǍKtWDK$3== b`( )xu# p\ENNw4 ١CQ;F,(Ŋ_K`E _s} p3w81.WiXK.P${s@(g@P28 u@P2mҏ+id>ytե cEV*=8xS ,l3Vg%:1{kGT//1 dσ` rDCt2TU p`_/T"% –gˉ1>L8{ ]/1@gˊ1!SC02ȃ+aBφ@J 9 |#ԛW]`w%UR w6 0֯IEP|`" %&ONxSC !` ?w9S@wV`/6}IĀqUN\Dp&-&Wlx:Rd) W11"tKfOi>S:}%|wIz`I;O]e4 v;?<ϝ Xɐ0Ӫʞ$x JrBa)WY x34ǐ<\g&CҔ?Dc\ N#aQpoC#t!31 ?0( !2u RcN>$ <ca MX?42Ї GcᝣHU/ +sa# 69GIO8x)T!~K )E =O8W@g z=Jpb 41NO>@C1l PU~\ hKķ1[q +D`aQ$dcܷ!8];' ϯAkKˎ_$OP#llz*0}X.º6;]x/ 9Qx1CR}h.RG^(14`XIժ9TuY0¤xNh` ;ӀB )= h> a$DpxE%cB h ʼG {<`4{>4SdE2#rV 86$28}YkUS WUq)(믆3|Kb  "c `CYC3ܾhon, f9640f{a *D?# ^iG1cx3ޚXC<` d'jC"7Hd5 ' LCyV|BRāzcbMRPk{ ֚p|3e< +P2Q<H/9ц+mmX m#4}aš4D@-Xoz`AM/la9`?? Ws-gv8{υg{,{X|e. <a{o'/~a{apmh>7'80x3O3] Phv.~ Ƿp2_Sk (d2|3/|tgO2`8`:p;vfgWD€$|kok kʝ|?ζœx7~/mukg~ f !f2,8]0C78 0 1u7[GZ])S opc6ފNqQ/cZ`-?mPfS-/^] %LE C1M`?%Gyr6ޠY:vT EK[{`Y,V o\EwQP0E-0fAK@7L{)_$3`{x_x֓|8O@Q0f0 r;:tc $F&: VApW2ӷ`  ӻ$h_*\#K9`3 `(kG:C@2 €5X-4\-#F>e毃7%XC6 lōpJ)ċi|֣aIIJ*)2? c>Ux dkQ$/MUkS3rr)Cϗ8xoUxWDR[ylWM|fTe'Q JW ` .p9yryB_)h(44&XW`tہ`vWkfcD kަعp/e# J, b$pC1C!G~_`Ea/6St/K]4`pxGoT>Чa0|3 @f~| *DhY<8f4!^pd Lby>Z1 ­B|( 0T@?Ā$u!$6Lg(@P ll+( P / ӂNǪ8`H[ΆTU@B.!$\š^X}%ᐼ3 Vlv^[Z7dH1$v^=;9bc8% pEtJ2J| y%^8a# yĔ@cIupB`fBdZ = bNQĐ|h18~yZK۞xWC|OG!*"|À,3@ 0G c 8G 8)m4;wט8N2]@XFRdSq+ ܳ;‘xK.GZh%cx'$PvY;^,&UC ˇ/| `c;9 =Oh-$:`9)PW< peF@hH  Ơ3MT _L`:GpƆw=%t_P|? GX)* s,r\aKEET#14(02=p0g¼ cA) `0$1~s#( CsA E$3|/u-d3T"Ϝ/i. fVc|?A 1\ϡ '63f|8<φ3Ϳ׹f֙:h3Iɚ8K6 }ޕ竨ܰZ}trM}WGKꏗW%!01KS<όp*dƀ"Cj)@p@ oS` 0/xc:Y1m CO75 1xc6W׿T^ ǜ`XL3\F#:d28;淼 Ak@/݌3|?|]l2A{n{)?comom2:S nJhҰ{c2c5moAo}3/mP[2ȶ^5`߿e~e'/J /Ool3[~Q9ۗC'U\x^_zG?'wn @|o`OVY?=Ty̵C7hz8G-AtxduvaԺ@NޭIwha&\<3 90V/+Ap3:~*=If04lDLlru82qɀJg.5V9YNn'06g%yo(d2e {מC(ꏶ`4?PoZ6Qgk,G a8G+ǪFcN;R:w}O͝/a;U~S<mPRL A(:K 4;2]"M֝Z4ʃ€$KUx'*}{AQO2GwLJiRMf f?~5*q+,ՠѻCeZL`|JS݇R^q{q+[6  x? ]ȼPgB>N>( +g7FW˕3>'`a ] KZ4׺"?\x]xsa\`*q6,ڐ-7' C,Z&xe%l:緇B^IWKy}ϖ pUdE&}&Nz$!$p8|S aqh1Oo q c|(ۨfNAc=f"HP >"9ϢHAC<: t{PBb`AuHF4 `(~KGWzx g1U'Cܙ8kgӅ4zǰ6,Պi *tY݃\{pcd4ݎgm@02]_@@d_Cmon9um?m'3&ijt3ꍜ;朖ɰcuA!>}6+զNv=jܯZmN;' g[r>(>Pk@ ?ɶwfڠ_{_k ?18Tf<3 鿖 o0)^IE?_pxZ /U?npa_1p0 p'y<(һsmiXe]m; @3/3t>z=K c\ cfگ+1=C*au2nG+1B3?^<", &dvcG\Nq7`d}AɊ{c1JsgkH3RG62 j<] v0y5dÉ[AXXu؃ K?t3 oadG < LL"x_ U jFPp1;JqFG_w"Ϝ @gb>/] ϳ= UQp3 j=i1_WJU*7Uop*C:^JL藽{OjGK{UAJi} c}bV¼ 1bes':?T\{U|} @4xeX8zR\\h{gݥ{~Oz&v!e2C5oK0X#U_?_jpf7^x՘^A+؛2wm  x4q,$ofvh ^ q4^7\Xmw'aI6ا*xWfLt3 ׏(Q@y^'PY3^֘Y9׀arcgn35jՐd1GcDQMS;KE\;UǟE O떑``~}h–O>4y˞6>,O{q>^3GNVo%Sr?&1lj<Ѓ4jM3>+fJ dahq~d;+Ђ:iowk8[Pft2ږE_yc.2goosӼ~ek2뵽w:ﻻ[N6v hW5rM @${מgVn9w0 cW޿O azOȑ3s` cFTXJm/8@ `/do/xo;G`7Op|3衲ڈ2Xxg[rXϋwk I<nYϱ۫K4y q7=g7 $R=甧 gk}cӬiH1<~.nT+x oQiOy>J, .}Bɶ:Tۃ$ǒ4|m?%| 83%k$wnă4'R߀,2ytcD~2n1|ixJ2uu͹;ӺN}U>}|2sP<.uR@ Co%0Β>r'5aڀxU@Tp<*|@d^] #ȼ'n_Hx ),;l YH_7cW fN-]ܭEc6ӣ@tHW(E'fg[3‡pÏ<K { /=2[}Z WQI\s{י42CG34G{Qz lPd*|_ eyu}fN,ȐnTd]VxGN ||l  6*]FaA?gPd݆cil3H5 `p G> c7(¼P>'j0| H0a$x}T A'Mg{8 zj)k?{yݞm|0FJtc<{>)UQo2俶H3wi\ g` Y [u-[U`|1owz g[z@c]_cwo s[?nQenEd*ğڵT5̼c0kwG{z CJ6ޟ԰c}k2g<ڹس[<gwwi)g2gw ۸x3umnqn$kiĮ_We+E-eauH4L U"3z7{4 YUNJgD}!J25i<ս*?sg䁟3×Dv5OCh>l`3n>}M1z h10(2R mtԾ=7 s Hց}Yq/Gᝦ(dӸ- x18c:]#QyfIy׫d@AErϯDw8 > :&1@]aFSGq.HO9wTHbfD`p):hQĀx?0tУy@g*`~ ǀq8͂iQ9X"$^oiYƏpY= %y'CѼmg7f?0·^6fͪw% sU _jMQy `O ?՛'"G|?ɕ: a`4xaJ P$D\ܽ{VLd1NxMa)`i }םn1T2\ i[p i\H:ޛj»fՔêy\{^44Yf{ =\Ê2{٣4kg ΢+ptm:#N;ڧf!:|o2&Zcsg&^cojsMͻ/s!jTyYwlhﻆ3^6n ܎p mO' v` k7ov;UݘKC?p_X3`"{nb+xӃo7мP޿w2 NPcQ;^ n `3dn c\<`_ Aadۋ gkDܹONHˏK f3m'.pN &WL#2wEU|߀,^ ({i#씹i bfE 9VS{)g܊^>$(d|3n7VXbKJ 31A$(1! 90pEs Tpy钸2TFtJҺ h˓W@/rWwީC7D(V?>=q=Pʸ#ǞXiVG=5}2u]^Q|>t'缄V3VG߯ 1Fh6ԈxiH5x=]#,)q. 9ՠVar78/A wcnxRZ ɏB+ ^,Lfyᓍ+5F @1*(c5'/ȝ^̑ z Go5Z|`yɄty.vjGN[OROk݆ڟPW1sR>pgG2V HAEQdtkO*V n>(ܜmm'>^wS!@d/ 1pc92zdEcZe 3jO^ޞpN9noȹX1jaI>6v,o W"tDs0P8N+KEex}Ϯ!)_+ .%Fo579sy&:} 'b|2U8Y"vUsOe lD]8@JRl˙Ȏ/m,N`!n#0^7ژd{m]qfBg畵p 8pdY]Гŏ6pgs# C fkyxc~+1yUVp >Ӝ'};; awW9¸pQ(4ˆ3^m2v=_1ٯ#NoE{' gs ` ׼/a8 m\~ 77VZgx}643we/1Ս ` ;{{8$?w  }1pZ] ŷůc\-Zcᕋ}}9?1;3I9'ܱ0|(єW @ Up^O 1Qqf4Pu;5P+JH}Zn} \2[(­+a9tվ|yzBt!INN`2?{lW8PMcgKcm+ڧ1yb~$@qywW#DžHIkb(@Γ[5-O:OnIu&x~_uVlxnIL+ 81߁|Th4 /_U4:.qmAѶ1 dナ ݽV<:)rPbQBn%Fr@4W;ܯu J > 812*UVl1Ru3E2"'`6Dy cg-13 =-!b`ă s}H`2W@\.x3j :P2 81_jb JAW}0>\;O'}QeSx2b3]ϷOpۦ\)k<AϓbpY>81mm&2qӖ8TL$ȾX!ϋ{n7|~m99L2&ksas% OlkA/}𷊏9l<>ܭIN*3Pdn 7 sV}!}—>;H:9nsbJ8ll0d@*%&ݲ0x|(

    xD|^N2Px_``?σ%|* 66J|(

    lxd@2#'$`?΃%|*662Px_`n40xD`|X<x_`0xA@2#*Ȍud`?΃dF̀U: ?|*6VF0xD`|X<x_`?̃dF̀U2 ?|*6W_%2 ?l*6V0xD`|[< x_``?̃dF̀UȌd~ lalala

    L2Px_``?σ%|* 69w> xD|<AdH̀M> xD|p<`ATJ̀OL`?у%|*Q(662Px_`o?у+|*Q 6<2x_`s0xX

    =6oᅪ=S<{۟g\|3<.~6o6ohmc~r6omm6om9|m$gǛr67y>nq/ᅬ}o7cmGmmmy|6oǟ}9}>#m7o?6mom7Mmmmmmmm}7M{moscmmb9Z96۲67^7omm>nqyw6^6ۼmm6oٿyo;06ozomo8om6o96mmm}mmm>on~6|io7sv7]o>G?s|m6m߿?|o8Ǜ{F?迏m6߼mommǿ~~7ۜom6ۼmm6o}v66mxܷ,}cͿz~6~ǟygY[Sqs}m8oo;~l/[0rdf\ ̓$k[`sl[ &Zmmxom{m{mۗms'td@ H^,`TqS3  `AƷZ {is* 9mmǛw7-6ߜmdm`7>2G}mymmH #Bh X bʁhf"hbe ?.Ze, x8[%>-L!Vjg=]mmmmxnqmmmymn[Y  BC@| HU>cmic,`3 CJDAnmmۍm?/^7o`Moo:>o|oxy| <3Ȃ VsȂ (A _ AKB8<-#8^6mqm}mm[m|W=o}L 0<s`;q #::ڱfft 8 Aq󍷬7-6ۼy,o7o`m#h$IƇ -d<^L)[ &h;bP$@: G@DE ` P VJ9 ' Ș%L%Ah @d> 0-Ho2>+ ۘA!cc@%3Do8O s[H+@-dAwH. f@75yebRUMa>P)pFL9R! 3Z 0/R|mCppP&/H:я ieM rZl49a)HXaK&X\>`^ %3) (|8,tbe-Ȇ80.8bw@%ekYmA$PJV UM& !'`֨2,4PX4ZD1%5Xl 08 fP91A<_!P|0%$ ~Lism l͈H03ccS,40apZ0Gc.3xz %yp \ vp*HMb0Lc0-!WނCN-}0|? Xaўl2lj*$&g ?Yw@@t| reS@2 >4AV< ` a%CSh X9 `1?(oLcΜk "@Vu,\>tY83 HL͏'aQ-kEFT0wLLXo v;KlHL |Xt 'p*T<-lg0vL G@@ L Ra>|-d42ʰqҗU0:Za;ʁheq Z`p)[WB͎L #0\텩Z)HH a%#I@X+00ҖrR/34w`<>l~(>}?%L u#ݿ%@<>0p)8Z1:-x%nc 6hst \ r[` <0|p-r̙N;Lnd`¹f t%kZ B˚82b֌ 3Sn0 ƶc> -a2 3C(&s2 e>'hrch-oL`L>Va| _Mx0pTd9tv٘cH b`_@|E f@ ;x(Ų%a<{d-UP%?A I Z7A.L@ AxedFXgm6x cm m[[x?ex?ixq8L~\dgpK /v(T3#`q x?U0t8vĐx8U6$2br:Z`t a`Ɂh5 p8pd 8JO58ktm@qv :xtd~ Au`+.b-R 9s8L- R`FI `,[dlB$!`@ ˄!60.rT2V0${2 `p 0|8z%;<GX8Z0<Uh8c`0K5&ʅx )ȧ1F al2 tC<b>0 0H0$PIũAQN`\K`(i#0 X\Xlslt 8[ \#;x rAǀ%? (`As$V.N2X%d.Qdl3p aLp, &ya02`bӶ ZR cZ ?}a= -J`8 %1-,c+N`p8d8A!lc g0Ѷڣ>H@h^Q^ s-ٔdFX-x0 m#7pN66n ނOH@X0-:N P/UVg2x?Zihc*q>.~)A8ǐP:0p*ՃlgNXJlp#[( <m)00$43<)@7@)ALvXc8I/'J s s?@ÀNd}s^oA#dp|?ụX8Z1<`U ֘-gq)P`Q&@_F@`PӦ5u 9q VS>  /-!H3Cp)(_h%[79ydl3@ `q8^N ~0_2]R$|x?EX[06 Xa;[@[7 z`d * pA S;`Pr H7 X%1 :  1<\31{K~lx?B\ &L(Sfۓdc_rbp8 ?z qP8L8 ar$6& OĆx€dp#61>ZqKӺcT -2 - 4HdK3.xK 82Thv8[lַæ5M`LA0 gk%lpniz!tʖW0O=9[ z2L`IƁ@BsF$`P'Ba <_JHxaS,*xp0([$+G| xӁ1b*BfXS >b%a3@|j%w'mP82`l #FaK:h pR$ 6h9AL1c?$C=,`aF0Ӷ;qS60\x?E'*RFl0  |m $B8K@~ƶ:q  %9@Hs+ HIS 0B k+{P[/Vix8Y%,m˲6ۜ&e۶>=O|9N3G@PALaP #f@.oKمsH0)8)|ĶxhU0Be>A`KL A0JH )Ʊ`tv8( i;xِ %)ְ33r,h[A. `@Y&Nғ{ ðf-`'`, )Ax( Nv ƧoQ00|ւ@0B`e !!`&JT "Juj vU韏,v 2lFN 502Ci13 "5[[l(|mpgdt!['N #fAI /uͶ60' o2-hYM$p|.€AC$ȹC2Ma `:;d  1H0%p0`@00(@&%ֈ jg  à?u7'0^h ҀV  haؒ3a0M4Ua`U21@|(c] 1Pdt90 M1e-, [ ,Z ?Zp>Zv银T  %5)p pE#)S0E`6$h)`FdɄS zڜhfX2ra!trd6N}ReNG}Yr}Lړml6$Oƙ -'iR`áeTd%06BҘ(>I U:^`64 j>k@H iPd<3)zTg 8)tvZt0) ~Ѷܐ(lV!*2 <Yh$@< #ٖ \. s8 %NZd˘Ij `|ƅGl4: . lvZhv :HnL;XK M4#RM A |5 (J}3h\a$pa<a04ɌSBeq 2?dqv`{cAj %tm CPga  7L4/"@7˙=ZE ,6"H ^]5AhL o'a0*^d^m;b :gNA |Rdp`$f 6ۀ%{ &`" a LDSH\Nƈ66!4 cІ ;hD3% X.cp{Їc[Jov<۲}8n6d^3#,D2Z3%(̙͒L#o6f7Y dY0-N2moBe2LyymZA`2@L`%Eφ"8^!eS*=T̶T@Kj,!2r#ηüeIe0Ȱ€cS1)Ӎ̚iG5~w I_4As/h˃Z92:!A c/OƮɿM2@0 t!,e0!@{`LmIVe#XoXo#׍6o7}6}m6nqtmoX7yxomv?=yoݿ6l=y|[w9yGmmomxom6o67om6nqm6om}mmyxnmx{ͻcmmmomm7y}໰<m6766xnqcmF忍>omom?w},zuxnqt9|mxoݱǛ|6mm7oymyq忍6mmr{Ϳm9qW_y>ۜ{myۮG}ommo=yo7ܿ7m}^>zqǽ>FwN>momǞm}Ͽ6om۟vmmmmmmm{6om6o?6omoNla,pxD`|X<  X<,_-xAxX `08 `#`q`'F %NHo %NHoK+7ߥK+70 8oKla,pxD`|X<  X<,_-xAxX `08 `#`q`'F %NHoim %NHkK+7ߥ0 8oa,pxD`|~a<,_xA`#06`'F ;+7 ;+7h0pxD`|~< ðxX `06`'Fy ;+7v 8oNa<,_xA#`iNHn~ 8oNla<,_xA#`iFHk~ oKxPa<,`_xA#`eFHn~ oKlPa<,`_xAFHo(<,`_< `A`#Hr`cJ o80d$`cJ o80d$s`0/%+(72xXs`0/++ 72xXPom6om6om6omm6om6om6om6om6om6om6omWTn.<>u?>/<413S6_%?1϶U;v+z l21 ңv3m+•'&=pܟo>> Qܱw''mٷm q'1g՘7Bx{n!Vu=fԽt6Vq\2]a(${}z{xfCa1ំ<6ap>³/pxgDG<g /ca!c">엃ds<ӈ%; v9=1 4HXyP>'g*O;C'6۸ w30u3 5X9+ M-!}azD#牐gCA7OU$B=|/ِfqxf^7CTgǃ5+6x,ML 2 dN#AY:u"`d\2f>CfG+Yd>@$)(PL _0 ċC?d>>g VH!qm'80"Vuϡg՜b㗠2S潡 wh3?r')m6rM$1ʙ?0T! g>L}i?>k<{f1mWM_<{01<Qo jac cگ 잻uq'зl2c `ޱ^y4Οe 2Peޛ= Z(Ք[T=V$?O=n]0bE -2} Ya.#k cmcN%O>fy ~PɊ ߽\LHcgIoZ'tv2taW(}x2zf޿60q{Zg۠ʮild6EaMo ,^OOZ2֛p1`SnBpf5 r̉7]O,^1mm1giB=ft>>>½3ϡ6ߦ}L3 P1fX=Z-U{I^p`ۚ'WL]ze^%H6tz/sf8;N:(3:'<܀XO<~{Д2=?=O7M??|?|^^+p6V'N^No3BJ`1mW]ɂeP_{zH؛4~{u'3jGِf <-ZNY~6獱Yxk0i>N-IN2_>}Ҿp<. +}y?&_Xf?py竞ݓ {ʮ+t~^t-/ YwǦpj|qG‹%'=A' -V2k &' ݏޅ )2Iןdsqc>aSbσ N|bcOvwY)1cy}JWvCד' _ 6.G&ଞ8N)7 cF å33cz(' l#/Bݟx}I$o5pb`ܜ=㪠\ ce%K#rVbPzά@ǘ3'f@.s!<*Sݎ7=>:v7[q= {)MH2xd#'s N:kXd*!lWnhSA+oa17mޝ\w{k{owI2lj'kߏ8z{7[wp3c?t,dw"s[kwXX} _cNJo kN? :`?VڵfNcW?cjF vc`C*0+pc+:L蝅*<|X3qv}1}YGA0@z+=9/gZgg^MWu]zF>Шԋ&{C@;ᕨ3c UӎZy{iǃ;;cӬѓ4OfNv-]=^ x9<2Ip\y}X##۷4 J7W+mۖnپ?Vt2lS˼|~K>~f}:<{noY2V{>bK:Azuʴ|]L͖GέGWD}OvJxӳ]~a{Hw5X+pf=t]fI({s#A+ڪ Uw2|W&UWWR}^(Tupp:y_AF {.Xm0IId<;C*y3ܕ?SN>3'fz(1 [vA}] m߁?qսW9_g1d/*8\ < u$}﯎a`?L3mLϷA#ufDFLi nG:'{i2iu{6ox>g*$ Ɂ! &@g/0d IIkhM6a5 l>X腶oabŰC[aqYIZyPN&z-:m٘s>:9Ӆ{ކ3_w^Q|7'q^BkB;gҴ zqucw u9nӟh:=w[Ac-㥴s]lҰ pq=ʤ1kpͶ/]%`n  gvzQ1)ON?b{YI|ŁG_17fYf`p [x[I?px1'Ճ=Yy\/:g &2G.Pl;/c>UZ/ %Lf"nl)Q Sg㶕1k8c8ޑ̀x1mWq11dsh1+G+=s.'Ygp1; vi%n s1U'wa޳gأXOx׋n(^l$K1:c;OO:8ޮ~oA݇s{fu])wY\ҿF ʪOM%ƕzI TvSײu&s7iz %m1bA^ H/(ΉZl^]mC7IU11xiCGc~xӀ$1mz]ePTMdEHmC}) x<޶2(Lă,~ N#bXw<)"LS։CL0)xZA|~N/=d)+Ƽ2^ J>O{ j2?jJ y9#}{֠b#!^& [>3@Ƽ u7?}(ʻ?kz}`rxN~܁2鹷1{moW p˵}}˷vclb2wnuw;0[kw5bSnowdہ?ZC;a?߽ twz w++_+[/ fxb?ݾO'/w[r{- BQqj7^t~{]GcJ،2jd^ P7˼k5o5.9i[w:1t2DӼ`0(cvvN fy\2 q ow]%_* \ >y'n_p$}7 Dd;<~v%gj^v`2y^ f׋YV0112}:>:IŒkQ{J|Vo{X u 0L^|Jւ3JKfM y!M~??1MëTm_ѷ`وS&j7b&uPQ_12'g^[I6oVL@wͭm^y2Wq{ k`cUF[Luwik?Rθ^M[{LJYs끺j>ڻ;yqZwVu1ͯ7u,^c0{c7o1}Ws`Os`bCom_,1{iZ|@'πO6?۟_vg W_.; a{1o?T2~N_3Im ,{cKs g6Z@jw5,Ǟ; JupM4+ĬoU\çtЬ3 i' t&zҹd̺ni=bnǩ7_=]n"_< oBf2A^o9M* Ct9Q5Tqir}a9aZֽtKӶo/c$dX1IwcQ{ty@GȞNCh ]6 ~Gm. ={h:Af{d9:\hC7@4|z]PBsVz08 \ |A tlA!=Їk85 -<3v&cͱ[M9m-@|o0e<T?KAG缮U'Dǝ12 6⿺ wmH`'J/ p鬑<?Z*:jdmgۚ-nY=blojϽYb}K_'4񺱃z}f:a0m?Nmavs4wO4Y_w nSWxc6xc5n>ۓu[=m፾޿1_0e??c[̟Ğsc6NSa_6/`3gsag#sp:qݬ2$t2ν/{{!l"Vn hfɃxw갣a1}U c'_ 2 "ga@5 mv!U\i1Q#l2>qcnra^6{BOiӯ"t <֮S `VnY@!{Y:a- 2-d'O=-Oi\+{ֲ|O9d,1 s&5hf"u `x^OOy=Giީy7?A 9F,|\0 pe ̓klf)ց"\^6"cbιCt>UX1M+P '2>/|ڑ a+7] 8PdM^CT~`OxR-ӵ,<0F9Wv8{O $ P1红F@cF 8WTPq[9>7O=f^nt6U9_6ņMc'( (8*A 8`VY!j7vx  1$o/aBX005Pdyo{MzFF]rq~Qo񾱰l뫧u{`{Q7w@,﬘07Guy00":h9)It{Mmc8]LM(8vwV︓NqusvۀVq1j< n`_`[3 5C?k [p[^7=}ǿ^/X`1 }|\ O@ p1hY3c !d뜇LTh'3w{M׺/zXm0/o c'_]0U3{ Oa3|Y0cv,#2{MM?B `^b0 d7[6daaUF;%qo`F3d{gx @c$ᚬdN^#`2UЀ }>Gt32m*OVf়)cw)C}lV,FKxf/~ڽV \91ōB2 `L_hAwn]lS罱 RG-7vkG u"I0;1\tkE3C  `2E10sU H3 O  fApT`U6(|Cq|('I>y r5sޣ{8״?|(]d+o md9s1>`+Gİ%ΆdIyJ0mdly0OaqQ> Z*r1C7\5oWxbm̀O1dv8LdZU%Ƀ36Ӡ`MT m)Xo0>[X9p>'$WuSh28 J 2OZ Db,dP ȜOuߌ VOpsߍ*wAz{~!3()vZh2ȼpK3}T (\хǎ*m3 w\71ms==728ѢwU7cl+a q?b |_~}x,S>,U=Ͷ}n kCz7W;[۷ϡ8fNI3i:Ou}؇J5N,Or'}'wi7Oq63+U_0c2YC'8wuӏoV@U{v q~o?ǃϸ s0ͯc9p O_ gsp1J @| o{ n!nsim ywHc211m\8<{5Mzc{x w8p[ z-xr-z-z-xŽxXMo{F ^{7mooz9_o{ݱC}m?at~13 wܺ\玭C0T-iaͰ~m0ǜ }cm7c}6 ypm<~co? o?፷ m፶1m1ߏ@zxc1፶^മ<._sNJo+xǗ^ņ6۞=~ۻ;}?珏oaXs4o y? ǿ0ƣ͋ca13=,{{Ǜ0Ƴm}?yoxѵ}?> k6ط>ϣ{Ͷ-!r= jر=y}᎛lX7}8fŏzm}={;oosǿ}W(c5+> {f}Ǜ? ǰc}qxc0bǟG1bǟGo;xzq~~ņ7z }3:0m@} m5>졍tvpf{c`΀XcWŏXc_>ŏ {,}}3o  maŹ;mc0bCmcy7xf?x{Ǎ vby} tb yņ3]h3o71u𰷼x_? ͺ_~QoO o~6ذ~6ر0k1( $k^?_s[<s_>7Xc0bc]ŏw1bǗo,1ߏŏ'ņ7~70̳t6ۈoNJ'﷨xcY~mam m~3-,xOoFY EaحQ{@Z8t K`, C~x>o Hcm`ao15lXcm ycm_xcͿ tbǓ' mņ6*aw7~ƣoQ5[z?ƣoP~o^1X3m~6^&a!kyTxD#ALd E2Li_1ڰg<73 mo ǟ7oxc0ۥ/z}61߆>0k'}፶1ߎB1oPǛڎOy/// kv j}bҌo f8N 3֟SFHiǟB^iŏoaxXc3bG/|x 5lXcUa;'/XϠ+FϹ԰~6ǟ~Xc ~a>_=>ǧ? Ƿc ubǧ^xyox1O =[x?1 3 fycz3M(~7?{B^=iŏ1f=ǟoo<?/}xc+m#}Cx{ԯqz}Zz\ x37c3 Af6`1x; VCLN1 |3 ,A3aw,|xcWŅ Ƿ? qý^dͳ5vϼ1ա^=87νޣ ǿ,1m?2W5F0 _棋>4 qPS96`)XAA`_OS~>oxcm+j6lhȃM!7xȤaŸ8z0 eO9a)*(<)8~P xB/ {81ͱa>y|-w>?aFo ǿ,6\- ͉>.> ˁ  IEG,Õa؜2EdD\$^WӡuCC~?xcm덦x3+揋S "/`Šl#qLHs+!< kĺ , 3\9 P.*G8 "-1Ek~zaSǽ5P:a ?>_?a61ljo 0w /pfom_pf>>e } V j? yLJyX?~ƳoQO{mc0x??ǟvNj^*hdp>3  Ps  u X0f6E#'ǸC<l ?1ًF1}2_|$!@ 8@Ec)&91Ad]`I@\jd\BQ m m mcYņ6Ƴm/3 j6ذ|?,y|% mՆ1 j to jzz yo A7%[#a-i:`cPRbS ͷ9) x~r1bf3f>mj<8$AH.6$/bѾD>:-1^i>3- 71waf^1bm\_ 0~c<d,r-6h۟7?gP]{/Q?6 ooP۟`xff[(&"p8^nyt)0 p"@`re%cW灸IAx2koDl?,#1lpY d!EZ_Ìfwc1{{foͶ,1ߏ7a]o,|:ͺ<̺1? pxcVj߻S7}}K{?1{ޡ(f7>^Ǟ򷁚| Ec /{KD O5B1V2 mhf>m{›~$ A%px>\?2\lX i 8aa4m3,AaW֏ F3M@<5Bx-Xආ3  Ƿc0Ƴm mecm m}> 3 ms]>8czgX~lxf>cxkD-x^h~>Dhb0酙X(c츝qQj 0.ePm`wC{1{|35lXxfoox+}3 m cmHA~]1~}^<xw1m{o)<|c`ҵ^>$oJl~r sye^x}3 1}[ ;a=p'`,IvA2Rr!h ƨ>Ѳ:|l\,4 }9Pb1Ai06O fcc1bycW6ņ6gŏ 0~o"'tgb-O7ooxb=Alxپq k:چ:ywc m፽ H*@t,D@`^X "U-;_"Q t𼀸}3 1}o{oE mFcE/`i a-Pr1HE @co0klgS7/` .e`^፷3,1߆6Ǜ~omcSlņűͶ,1߆6~Njocomcm mሥs {ۘc߹}y^P~5AQE,<n>~DU(2p C]q:m@>^8_#d`",~o e`kO Ӏ B(1x 3 _Ά6xͺXcm m61߆:maxnc m፶q {=۰.7Ƴ{Qo?o~g^es lrd Xi%X3 c_҇f< Ͷ,1ߏ0Ʈ,1ߏ 'xW,zf>a1w g፾u y kpxw5z*t{w,>4GiP,abZPXմ-7w玲 34^8 !}W!ŤG 78} YG<j`ꁃ?4;4%G. |Ei^'q( /0[\|ZkPumb ^^xAV KӔ[cžoccm'y~6_ <3c63 ub{cy]~; c޷7Ƿ?xyfPoh:?d?% o| ~0*~~<2 g10Xn_pf? -yǝN@|/&4Fco30ꁍo ,1oxxf? mch!opf6 ƶ3 m1?jާ፶1635coo{ǂ5lpU x6ذ~o1y2 dam m፶ m፶61xcY<#? j<+=Occ,1߆<~ozy}GPw5a֣x7~aZ<.o T [am {0aŅ {cͿ mz60ƻrņ5lXcm6~ņ>c{oxcm m፼1yC{cm j6 zM RͶP/s϶P31=^ kذ~o`a. Ƿc< am mcm m፼1mco׼x,_7aVomc}:ͱa1߆6~~3q{ǜo8<8a μd7ex<{=30O Bf<c<cc( xf21{6 <-O<-``oz{-{f<3}>X<`mmmmmmmmmmmmmmmmmmmmmmmDx_ T<( $Z|8 @6wFHlĐx_ T;$0x0pA-P>l0x_-P>lHlN0x#Z|8l8l8#h l8lx#Z`|8Dpx_A L;#h l8llx l8lL0x#Z|ۜ8l8lF0x|I*@`pA-P>lĐx_ T;ݑ $Z|8 @6vHlD`"K`EAn<}6mm6oyo7t}qsդ>F}7oǟmm}w?o߿ommmm7y6m?.6onrg6v<{˧oGyms~o7ooF}<ާo^6ۜm}mmmo|oɕ;yGs^ߟ[ޱ鿍7om636m6}Ӎ6m6n_mm7ct,{N77|{{mo{ooG}7om6o}w=o{?6x#n]֟Ssݏ_>q+C_*ѹ6!R}W0\3oocqXom67nuN c$Pde-6r0[mVY7'ZS)ѝ2,}{Oom6n>omoyxooo?6mqӺ:j?i֟=9o7_m^ع_ %뎑 XJ1tk,W^mZu/_f[Os6{m6}mǛmmom'1I?cn*qǟ{^<:v=g6v7_N6=uadym6޶6mcm><{}?6}6sco?L dz _z;_:wS=>ryqc~Ƕ۟o6o}Ǜuoy#mcms>6immFӍmmuy6oo6m0z}sZ~?U=Ӛt=W}uEoJm}ym>6d)zg^_W Z`0)\󨵴=Duc`cVyc5Uu<- 爍y78moo?9QoRq>56ۙmѷ)z>ܞKHxıT0/}2IcC a<4x~ƒ˧M<$I6m$7,mr+z&ԉ0@f`Ŭ$+296!8hr۔9UϠǵ TiaD0NdW01/8oosmmmmʠiw5_Oο:gO~5_y#]8~1qT^Ok@ɱI)7[ z7ǵK U0lKFV 9nqm.oCnruɸjD%񁿼4:Eč6רe1Ѷqц8M*S뙕Jei,0MOGmmommͿӌg~+Rnݏ֟33M|}o+ϿVc,THc09l_dS"FY2l0ƴĠ e$dS0R/Ll`~\X:$m66|OuW !4GH1IG$o{ˏ66fmmXSom6omIb*\=mmxnm8nomo6om[mήbg#xۧ8T}z#cS{X 18a6Fq2) s]M#onmm֤ybwêP CmYطs%(6N:)܏2 !=Zlcq5pmwm|oymymo:ޗ"i:?}{N[oOo񝤒t̗:8{ J픨>$e3_ܲIfI^tsNZǛsmmƅz^Pp 3O1buȣdr.lLyGe1yXZM8cWXf?cEǟmm/mFo|m۞For䪌F1{w֪cOMIHt1sc-XW\- S (1zc>6- M7 $LZh!/w6oo?6{o?6oOL_C5Oo|8~FuD}G@ 1q"Y=;l^0!ۙ.>7mmqڭ*yk)3a z0.g>2=!m:ٽ3` p[oӍ6omz[n=߳!Dm,[0,in3 ȩEy| "qTt Bsǝ1a[!b'S޾Tccy6^+v8 Ӽ8D3*De.6"|/9Y>S<`1XXpCKɏ'0[Y3u0p!`n,Ѷqmomm|mNk9gM㩪zgN:_#wNW=U[;DxNLe30,on%`{ӣ:E aȬtyjGq hX)'c5ÙO i9̰ ?H̴BƗ4 Rq#XKh,j& Lu;;րa5gt͖6ۖ7ߜo9n>o?7m6ootR{v?Uokh|㩪NW~:{}񽟐d8Y9! ܶd!ɏomYEz1C#0~Y{e ,r׷@Z7eCٵsFn~6mo?6o6{m6{>;sq?Tz5=3:ݍξ:s|6|ymgǟo|o7|}o|myI{Ogu55uc~kGt}}mFmmmmo|mǛmoom6{m}? 7|x-=8pxXTp xox8<, _8GD+u@p70GD+u@p7L0GD+u@p7` xoL x `P>oK|8<, _!"`|80xXAL`  x!"`|L` o<, _ߴ!"`|80xXAL`@`0>o<, _CD+tp `7 x!"`|80xXAL o<, _0CD+p`7 xS!"`|80xXAT` ol8<, _m0GD+u@p`7x#|8pxXATp xol8<,_闁#|8pxXTp xoNx8<,_타8G<+u@-p7x#|8 7$xxI<+u@-p7xxI<+u@pA`P>os9$xxI<+u@0ۖ6om6om6om6om6om6om6om6om6om6om6om]2|c j7ܯͶ$1Ͷ$1WmA mm?cr mgj_ƣ}6?3 $1+*֪lWocQaT1mϱ_cYm f?lO f^HcͿ}_z f[lPcy፿6 }ᏼf6ƣoO*gC6د|1{oaw1mCnxcY tbc'o>n1}?aJ o?g- m=n f? flPcm(1A9̶nmgؘcY!፶1m? j6ؐǛ~oomKc8bCmno&Ͷ AܱMwu k?^x1߆6 m?{6!<Rxcm vbm|"87>ӧuupaf'6cL2|1m6}B8 2vݯ(cͻmncQ oOo:1ً F mz6۸cm t^輟nnm1myl ɀco^omcmE1@`91od&:1mCmO1A8Ec6_I gjN/'61bxc {6bO/<6?5lHcͿGm溜cBCLOfo?``e^Hf1 ,7﻽m8c ay[ vm̡no/{/|1CmϽSNp fs<1baF m flOcBW j6ؐ tb1?tņ7w1 flO} j6ؠlO f[lHcm3 + kخalNm f_L1ٱ0~o5vlPcͿo]S /10,1,15[l^|1bc5l_cQcQc1͋ j=ؿcY{rma6ƣ{_'{m k{Vۗk~xqt3w%7cUx5__Q{{}1m/'3O,1፶6 6q;ۗ_5Է?qm1߆6 m'vrl3co? jxc^_M; 8]ۼǹ5[t܁G@yW;x3~-۷E3M~oc ~?8/ Xc0bw/OW3O}/nл dUP~om\> m[3\@`1P~omcmG5l,1߆65[l_$Qt Q m1mc4ۥԝ2umೝxln፶naܯ؞}e3Mw m%{qex}qy_ ~C_ jؿb}>k;.[dMa1bhHf1m>ĭ^ !m{sC2G.(u jذ6ls0aj9 <6p_s፶p qgCw1b<[@WKf[L3 '1m܁N}<;ۻ-0cӜ~V j6ظcm$xs=;?vt[mxC?{NLַ߱W 49Wǿ1m f;Կ klؿ<1͖o k:̯cYm/1ba^,/m|10,1,11bV1b//1bcY{1Ƕ;Bƣ~#1b[/c 17cUƫlW?3McQ~m8c4bmno/ ntMnR1ޥ}Ӱ@5ڹ<_c <~፶BӫycWKx(y=o7q{2՚pʹf~m}^ Gvc4 m፶~f02Ga፶5l_xx5-ۅo Żl3 dUP~om\<ᛘ mzt <3lZa,1߆6EcU>}|15[,tdx3ww~n፶o+W6ۿ}x& CP 3F |D;m~n፶na? ڰ˅oo71m6۸cm.I}ADc4bc4m2vŤ}m܁G}}y<<wn'\vLjgݸc4ۥNę6{ j6ؿ}8Mpnos5zs1m<Ś<- c; jذ6k{OpN oct4v7ЌW <.F+'UW>pNwwǸ''x875 ms}'}7 {OOnێ1mҾmpƣm6x$\}|1T5CwN/B{cQx1x*pL k-o_1?}l2|1ez}}apC f3^~ؿٱO7AW`1epCp f3졍p3p3 fln\1ٱxc0d5l_l3Z]C@ ^Tp\pMJhXfw}h< |1| aVمWoOۖ~k᏶/ y{h3,^ f[yanOl_7 fzp3xp&} f[l_ f[y{OfܤecUr;m8'}}\90.ml_E|1Ͼ[̶l2Y3 ᐶ1'? j6ذ6t'g/o ~0Ͽ{JF}Ϸ 7 EoSӑ;my<3oj b/jy4ùӆ1ClP -4>=9&2t j6ذ6}d5<\ݰ~Xq@ݻm{mC&3m1m6}Ox}X1XfWc]ņ?cczޫg=(ݲ5N fv\1^?፶{3p/ ~2xcW/ }׸c{?}}2}ϲxc[1bo[uH! ެ;3o P 1rm1r5[l^n|1bcͶ/ vbƳn/cY_ƻnaƫm=mn jܿ_c0C1/{ jؿNcUz/ f[t}`s? jno~m]Z̶{s1t wxV/v kؼ1uzEkcmnYp&لv.A^ɏ\1{wkZ(tknx~=5^_cYc2۩ f[t{7vk[%Vޮ/omq ƣm m=XW`{t 6)ςiN{v jؿ:vh~oww1bLnxWWNjΆ3:\1ƫmݓ3_'?b k?ƫmc4ϲ8?v'[[P [ {aa'vO7mxcƫm6ػۼ1݋m1݋|1ۥ}xnp3u/^a j73vO jؿW1im \1뭿}; jcUU෸##1bOc]xom o_[c2/5[to ?~1bwcxWO1}x0c13 XcͿW=?1bx6ex7[ v7ܿ~/&cU}q {-kgVc ݺ jؿ:~3/vO1m tbOOovw;Q{< 1MamkwY#+pc_ ņ>cmVۖ1 Cw,1,1ہ|xOg5[l^n|1bcͶ.ϾP6ہF"e81tgƧy>0171ޫᎿlwz k=طƳn}oVڹ#0Q>1c+ s';k<;X1ۥ[[ہs xC'5쁏Tw}}0c kظyƫmiy> jW<Z\^6Vۗ1pכ 2}@cYQ1~>፶o1Ǚ9y<3x k6p+p5Լ0cc_>ƫmpO}s7cWT k~kpc3 f^1mO}ꁏoO1ۥs}M}agoxD1{7ޭv_፶1ۥyyW~!ޖ~ceKvw'cUKoþcYO oxI_a/ae5l^nO 41C& ;?፶O67>W `c6-d= ftǛA$<5z>^31b}}}}QRӽ{oŻŹǘsF_}}܃0A7ܯ'}0f\s7&y`1ޏ/v/}y؁1m&7d&ɭϾ'xXB 'b ^E5l_}}MIϾ(gѢ.h2zNkz#֯pJ Z{A=S b/V.1bk |1a! v2x] kJڽcUNwX089xCxuO-pƻ{//r:Cur۟|1b1{sco8 ki\@盄Xpƾݕ_VM/cQP1Pƶ'2x(DÚbOOma%[+/{P2{з:+u~koQX!`e mcu{:v3C}1 !Cmc61m }̓@;WoOۖ~ &gFopl vm<1ܐƫm msl_m1\ ፿< =L c@ƣޖo7pco jؿ kطׁjy"0Zzn /Cv-l1ۥ5l_:o9p;^W}w.mƻ kؿͶ/V@@xc'p=}ƣncǻp[1[ ;?c]K5y jؿԁWcޮƳWp= jvp[xT1@ƯmRm7q !>[J+hc_g1j5 jذƷmP1nܞ31b}_}w{[yCp1Wƫmm}yC}\!WͽNm{6cU}1z{v`rO5\W410 3 zc_>ƫmad᎛x ^]"_@yp5 jpƷ@ǻsp^ 1mhozboh f^UV/z}^n k?x%x\1эo,1KPc01߆5[tU-W . k"vMc f[tt#r7k5y jؿǪ} c]x+:#Ohc\,}Pu}{!cV 4v {~ɽ\W{Ww۳jyxg}pc?C}1Fl q+OB=n5^_W; f3=9yeKwWdwOvk᏶;qinzC^z6z/*҈9u6k엔~ƫmx%|1_}¡vޯ׆3;=/0 -Xs{Wޯ]W=}jedxcSӽ`*ςkbȏs?x( jؿx}`D|?x t%莈w k~m}}Yn f>pm7n{ma1b>u1_ k˪{3, Cލ5WmpQp1l_p^ 'z+ƶWP=uw}ka&pco8xc]/ k|1b߆:m[v=cWT1\ {1.kmc+pN磂w9[޳]k mRu|%o+ yVn obmc:mam{5[< oT1 {E kدxw[WxMpƾoMfk1 }gF;uqo|1 k{~mcQ }5z>mx?f}{; oƣmcmwSsU_}}b{]؟m|1bP'w}}vXcyc1፷f o}6حǻ'5Կw}u7c[݈1ݷ~Ͱ6s]ӆ6ƧzcY f[ b}F3 ]q/ƣnھ5n-c1gvg{Ld5yP3 }[=w ]AǞ] @}ﵽuu{mm} jzچ;s^C1y|1a f\1cQ{P/xcV|wmw{=aSA?}}ow[jG0coņ61}*Xcͺ^xcOF5[y_+ C=Cu cUcǛu]^(cmwx8@ !:jocSWXcЃ tX-pV;1W+ap_cUv'p15t5t|1WAg?wdjJ}%qn=ƫoWW+f-]vN3mcW}}cSUIgv[FwW}}x5 }l/p[T1wcZ]jWwޯ.u}sqEz_%̯Wuqc[vz߻E}}tGro?5t̂1ۥ}ޮ^G{.e}}qWw-r|1ۅ{5PWCwG2av,1[{W_ k?}wq_}uƫW} C_y6n}}=wy^ ^k\/ ԯ*:}s#vO}}Ƨ{W } k{φ51Kޟ j6ؿ;mpnKk _ j6ؿpE o}yuT7et~z=?x"_z}+^ޛvWK}}x(]wK_}z~m~ܷu}cgUvq5lXcmRW}C{{/ cV kؿEs}ǿ\/C=}\[w k6?So)1,1m }cCoԆ<ෆ6 yS<^P^ Cx< A\ zcޮ ᎟^o jcYw}  au w e af/F/{Ꮃu jgx+|1NcUo1G;f'1ۥm_.z긠^[G6ofސƫ.o݈﹈'owS56 owxc[?T1S5[lOt@MWnXcQ1WGp% k6 kfp3ƻo.m8c]Kc v}w W9+p\n }pƶ~ --Vx vޟb u1Ӛ Xc߻o׳e?ݕ^G^Or*a\*nƣoWݯsp_\*5q\o5_pƾ/tKc]K/Gdvc\P'u vVcQFWޮۚb?c{[ﺜ11ޟdvGd5p j~qavޯƻo-wxc70ks5] ka6cW?|1j}ž?}g f36]wAjv#1G-o߾Uy˽\N;5}ƻo-'w]̮ڸ|1js>፶ns=n }5cS x}ޢEﵹx5_qy@Ǿ~u#{;^<{\p j?xx/EdKޛwonW j⸧s4 m7-tKxxcӛw}sr¯[op&o3ޯW1( jZ4 ;j|1oݖK+;᎛lXcmO_ jguƫW8]x;1!xޯ5pv ub`EuXcr{=spcS6ϱCpƻoO/wWuC1U5kMW( 5+N zc wLW(5s̹@m F@.pPƳޠcͶO[[GLa-mmmmmmmmmmmmmmmmmmmmmmPa~ @0I0rK@!N xZ 0~ @0H0rK@'Gl< xX̀'H0rK`6;A `6F `l- xX̀%0rKp`6;A `6 `l- xX̀%0rK@0`6;A `6 `l/|A PKG`l=A PKG`0rK@#G`G/m6۲6moFmyǟy6nm78n|{omm3ǿrG{Ƿs7No6ۖ66}m6oomnnqo6}s6om6oO?6}6oc7^qi|mx:{>o8{mwzsm6nXzmyg8m6ۼm8om6o=oo6~qy#~o|{xoޱǿs6}wy6ۜm8߱m#߿?m,mͿm}mm}<o?RXdӜo:y#yv<xݹ|}m8n Ž(+Ӵq٥FcmdF=9#mrFmm{78oXnnXzݑrm6nqm6Tb_:No6NcپFsowN&Zm "eahZB1ءs/:2N@/eѶ/mmǛm7mm8nqw7-7ޜysS{k=y9>n{#y#?6~h/$0kB;ZJ LkY&@$ꐴ2'Cз8#oѶzs6nǛrm6qo^o?7m7osǜW;Ƿ׏~|{6t椏>ŏ>7o=sqٍnXz#m}7yhzqqyom6mxn}LF}Ǿ}|~t?U7>t?o|o[mӍo[ooov6ۼm8oXms퍽"z&,,ہ Y<ڬpg҃ h(1 }XD}0?_smmxooMVKfmJi̜dT.C5 qmͻsmǛ|zǾӏN7u㟜ZǞom{;@e EYbB``-C/ cokt$[` fN .Fs7mi  %kt,"ƂK10ȘX=5Rs@4 WDmT-8,f2 9FM8b676ۼmmmm*1/^=nrs;58xW}^K J41[ d S& IזO{`gNFo7ݱ܍lr-`Z/ki г %YF F2$Fcܰ慍YQ $makƦ}d{ommmǛw<ۖY3XyxHml~o㙩|w4#?'d4d L6'>(`4sAhZ4e;aX\VeU8$h_Ѷq\mm.|YI& (a4`u,DpH3 !hmƏ676zFi}<ۼm8om6om&vcN;9uMǧO{:zv߱൩5)hV0$AXt/cy2OHkT0³/1mmmma;.\bRr99p4W0&h 0XP^#ݼ4mǻ^ooG׍׍6oyݍ3ms4#k4_SǶ?ӍP.0L о6󕚴lTd)6xom6۔%T-izb6#{!h\ ǂփ̀AZi3pbVkAb`_ cǛjm퍼׍6{mFym9l3ziۜqz|v<6my$i%vTX9As[U}.,ty6v6޼m8nC>/`BcI,U !^4 z7 },0+ւѓar%a5pͱ#o?6om6oG5.UOǼWL_KnO):.0eٙͰHѕTlY5 Fo|m8zqmctC:4i.DZai&hZa  #ow'Ȳ S1!y#mo?6om6ۼm8qm1>{mw㚙ϟ[c5,`%,f /2Y;8^co٫Gw{8lYb^/ ML`h-04OHv%74/Gm6om~6n6^6o6^b w,oN}xT{z>unGǛg?~eH2%1!sfv3ڥP.Cͷ 772:qS?v9i'ɰ 94)8HyCh-e2x+ 81'1,vt,> < 0d$& jimN2R,.Iр, 0s$VaBֶAqfa 6eMl,> $-pӚd2Ɗh]10ွb)07.,ւ֐X4B%cŅ: \_ ;0`4xޅb:͞Lmlmmm{m6{:0 yJ ,<jc1ET K1^7ym6޼m8ommyo?7oͿ66|sr=~|r8ӞsgϷzf~?7OEdcab@ P J xfaqXϱ#-t͖7/#Ѷ6om6nqfͿ6om?m9?_{^9MǼW3S,u|w/=r>{bǻ]>s6mmFrimmo6mmoom7ޜo7xo{?6mϼo$glu>|sVss?ե~=[N6<}8}s<mo7m6o7׍m6m6mhH5oo< xXӁ8|p0_hv >o4 `o2 `o8 xXzA8|x0_ʹ8|X0_58|h xX甾8|X0_;0p`4;`7C`+hv >o= `o8 xX0p`4;`7> `o< xXo0p`4;`7 `o8 xXo0p`4;`76 `o,@_gp3nƣo/6dgp3 ^. z8/ j}~m6cUK[U}}M5ux-6l1kp^/_1O jg^m yz ^/-15;C{̞m{ whcYK m$1ۥ:m mRW}|1 k6xag-~溠cY k6ͼǛ}5[y|1ۨn1O6 }}Ưc3^G_ k6amCcUK5q}x ^GevuOͼ5[u?w}}_x-5yxc]ƫn}}}c] kc]5y፶m}}}~mԼm1b;؎b:n_m}}}}#]5yxc]} bN jc_ m7[}}|1guvށov]v#F.oo j~}:]ȷ; kv% y8cm፶}}2n- kذe፶ m_5ya j m1߾}m -dg m1 mwgdV_ j6|1}[;zCol5eHc]?3vGBaF\1_{}mmmb;G2:5[ywy}܎{pƻo.m廣u{0ƫo,1m}}}^1mv#vL11cm5t":xc_~ݐ1a(iFGݾ't6[&cYcv3 ̫:#1a dOv# j6ؿ}}}Ǿl؎mvKuwl_>۩ mŸ}|1}ݒ{.3;m፶}}VT:G{etgD5[yFB_cm/W}|1Q|_ w|14m m}1}ޯw> kf<ۥ1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omT00~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm` mƧn1nc$1Xcm>۫ vۨ:mn1XcCo|1~au?V/p o=֚cOnp Mc} j፶x uupƣ j{^m:o Y;{kpmm jt^/!5[z5z<1]̷ Ƴo.m5yA6 jcQP1ݘcޟ᏶y u7VXcYW1]ͺXc5y\1ۥFOc~<pL1 aFް21cUS:Ǜp\@&BB kmZ1 jO<3'^^naF/1|1W}uXc]K m<6ۿ@50 }\1 bNrqV_z;m_}}}1 mwvӱ9ƣn w1߾8}Ni̷ƻo,1۪nG;g<m08_p0*iӸpax\h1bm6n'wm6ۜ1m/=1ƻo.cU_}q5e!v^N ;117}蝭wq j};7}ݻ? k 8ƫo/c }.oj;m'bpsmn}+}߾9\2e?qkwp}}}}$1OTq1a(hpm}} k6}}'swƮ>۩ }}}K`rw O+imW tn\1e}r j6᏶ؾ$1Ƴ//cUK/ aI፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~Ud<40~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm1oO } } j6xcU5[yAm;m jVPcUoVW1 mƫo+5ym5zc53o km mpvW j6g7c] m}ƻo,11tcm[&av,1xcm jmm~wcmƫo/7}?1y`1- koaPp^5y~ bPnwm }~=v^cQ3}owwc]o፶p/~5?v᎛8^gv}vM}˽፶{5z_ﻻO<1W}}m5[y0~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcmUX4 } tӆ;δp3 ui }>>Y? j65[y% j1L1׼ ; jm1f?Lm5[yPƫo(1 mƫo+1_VXcm5[y_Cxcs1kV^v j7w1z(cxcUycO :z5;چ7m=:mg w1߷`c knXc]o>74;_8p1{+B sO[ uw}naV[01{ ka Ne`&&&י!|vXc]cmwwx jmy9}amW7cY_ kamwƫo,1V/ 8 MƫWۀ|1:1v]pƫo/Ͼ5[z &&mݻ1- k1W&'Ӳ~{ۼ/5y}n4' }}; b. v_5y7x;1M p01;м{msݗ }}/v'euw?y?}}}/ww-Nd= _}}o=:nmxp}}1}~^p {}}]}}Ƨ{Oƫo/lށMV^|01n5[y N׆5[t=pƳnnHcUK3pƳo.m5yA j^mm1 jfWMztƻnozvx j6 k޿ޞNvXc]T1܎}^mamƳo,1V_cmv_\1ƻl1m1|1? mo/?y=5z̞_5yw~aV_ jVۘ- }S }ո( c_Ɇ>c޶S`n |1ά1c1 oj1ϝ߭x EuO jx(o?hcYK m$1ۥ!Wf[6ꁍf\1 k6mm6ox5~GKpƳnofW< o f_ jƳo+^2$1}ok}~3x aWӆ5tuW t۪;xhc;1#׺'}wo0ƫo_v 3M kmy'd ux ^pƫGk?6m5uCm}wx+}avl1wDtKm6ۜ1-Gf<kƫo/}u=žvFṲ11};upƻo/?wm;x.K_ kd}}}§}W;F >mCWWb5zjz]OtK}}f3q k7}z][Hx_}ws1=k}G޿xXc_Mj[wGe7r:{\Vz}ߺ#w|1o8crޯzcU W}pƶW[x{}5y;}}1pƯO/:|1V/]ﻎ7WX1Ǟc jgCXcown7 RWF cyxc}aa> jf!~ A{/p1 n8c;T  m cjuu8cSV_/cn8cVNc_<1ۥo {cƧXcޡV\1˺D5trC\1^WA;s%c g߻mͼƳo(1aam_ k6莍f,1ͼ5s?3}Ꮆ~ o7f\1/m[|1[U y7ۂ3pZ~z }ǀ5_z8<ƶկ5y1x-}yo5[z pƳN쎩5tO}\v޽momA0c8-Wv-W5yaWP^w}ﭫd ku>V-}ufvGT1bm6n菾1 m8cU\o/% m1_ jcUK`V }}}n1- k5[t[1t%5y~n0;5K kO??}}}owt1;}}tKS7R:cQ}}1s(d1,wDz:ޟ0 qoD{=_uw}}}ō0 u.N}O} mzTt/}}?F}}Ǿ}@ǟﻫ5[t}c]crW0c};{u8cQP10ƣnccuܘc?᎟uaFO j?cGޘcUxm Z_ƯO1ۥ6ےmz|1ˆ5yAfPcm1n1۫ͺXcm5y_ jqww?m>/}<1c]KcY}a5_Xcm jZ;gv^}Xc_gn5yaWc[gK'_{Kƻo,1۪nO_s}w[mam,1 m8cUoc}x x/[Ӕ1ƻo.mƫn1^/)o5e!v_ܼ16{}nw#5y;c]1[% >m,ܿ}h5y?}ƒ_F_ܿ1彽\Ov]lnw^-may1n>Om?_k{'}>m1_|1:7ƻoOlOƫn}XR1 tީ8cQRoƣnpQ6 t׆;oՆ6mւCU 1x*V'끍Cx`cSo j7 j xC jCy~m 0C|1C {᎞讗ttͺXcm!V/px{fk j>;5ypƳo(1 mcUw@z;5t| k6}5= k67 jc]S߽xc͹]{ vvޝ؎5tW1R W}0Ƴ^课pƻo/z'}~G}}}c]o[]u}Kƻncm}}y^WDv=31bm6n:fXcm5_ڼCͽn m1˺om}}}npm5y{qvo}{n]tHc]qᅥ5[zK1a d-3v_}}[<q cvs>m߾GէD聟F_-}pƳoWޮtV/ ktDz:]Uu3cG} k9C}}mHc0}^>c~} ww}L1otAxf{ﻂp}m}w|1{_}}} w_k jC) X1cͺ1޸cﺁoƣnc=U ˯>۫ }_ j?F7cpZa}Ն5u8cW>?{n vx*5ƫm cz}fo jWcWcDƫnxk!zp[} jƳo/ugK5trC_ {pƳoHcUޯUmͼƳo(1aV_<1Wx~7 j<۴1KƳnofWlC}11}xcQ1׆5u?}}}u፽W/}}`ƫXc_oWގ1ۥ6rwdtx߾ܗDc]oK{}}pƶƻo,1F}K·xu5yavPr>}m#%tGHc]ņ6/ m՟fXcm}}pƳo[φ6^mV_ jw}}}፶oƻl1T^1w}}}^m[?w}}_5}N[ k}?1C kwr}u[w·1u{^U_F_ol3ѽ܉w>d}}|1׆1g% btVuK}}}Ʒ[t^D7 jmU}᯿=}}} / k_x}}W޿YM<0ƣ~5tcW>?5[l=1 vm?3Z_5[t1ۥ6ےm5ypƳo(1 mcUVm;1ۥ6cY;+ƻnƫޗƻno5z}v_5z{m6Nm5uCmmamcYnpƫo,1ͽ~mxc]mƫn1- ks፶'5y5y=1c6P fwm83[>dxcUw_m jGXLa-mmmmmmmmmmmmmmmmmmmmmmPaz `ť8<`6s80=Aaz `ϒpaz `9_0az ``90r̀#`0>|#`0?G8<`6}F Apx>lA |0=Aaz ``az ``90r̀#`0;#G8<`6~ Apx>lL Apx>lA |0=Aaz `{`90r̀H0r̀#`0;0r̀0r̀0r̀0r̀0r̀0r̀0r̀0r̀0r̀0r̀Ɂ99lz `lz `o`lz ``lz `o`lz ``lz `o`lz `lz `olz ``lz `o`lz ``lz `o90r̀#`0;G8<`6|F Apx>lA |00=Aaz `{0az ``9$`90r̀#`0>G8<`6F Apx>lA |0=Aaz `{90r̀#`0;G8<`6vF Apx>lA |0=Aaz `90r̀#`0>G8<`6wF Apx>lA |A |0=A0az ``97$yx{ݹ#{#>677m8om󍷾6qmm?nqmm7m8om|m}}Ǜ|6>t{lnmǟ|>=myFӍ6{xms>o}?o}xo?6m6o?7{o>y#O{#~ٿӛyymo?7s>mm$mxo?}qoxm6o6{m7X[md{8xǞw>mwmmyoܑ|??8ooo6߼o8mxom6׍6m}:n9#پ|?xX9m6mowm6ۖ6mFmm66nӍ6om6om7o?&8۱q6|{gc7nǿs<٫\`oZN GiS #}N8#46ۍ|ydm8omv?uЃ>LOW"LӇS1ѓ 4k@]-3#^6om#mǛ|{omm?oyǟ=в~>=7O|ݿ>ǟFo?o҇| ?h·X[#oǿ|ƍ6yo}6ۜmxom<ۜm7ۼo?odo}log8N7ٸ˒:v9mӍ6u>~:m=7?o8ϼUv9r~7_z_{qFִ旀a?A`(WH$`qemm}7U +'.[20 Xp dL<FE`1}R*| >,4,7c$msm6n}mj׏?{Z4co{??}6]i𰏀 b"`(l|T еVA഑dmmcmG}.}ٓ\ | xX5m%fYpKtm9Wˢk(H|Zmidc-d%m6^6n?m8nqmmǾӏכǝ9Ƕ?qL}Ǿ7m%UX@XB xH속ֲ VтGm6"tdX < HG #I@hĥL#.ax a8 h(̀cZ*|Z- ND@Y)0dm|Z@7n6N?o?mmdzyn:wq^c?=-n`X/` ILŧ~% 1m eXp gEhm>۵0`X`ȴ` qb X ͳ7l 6y>nqmmmŵ_E8ln~{ioϿkO[Gd51ɳ&j @9c`,C~3s-|ž"]VA݋Y7Fmk#mcyL,Wa`Yb~ IKHV `7odo7oǞwmmmĽőo[slu|{9^W[=N{Fͽm~WL D?nd` k>,Cңmvp 赍-!Xԃ>:`]4 ,Zwomm6卶>ǛrwkGqSڧ~G>;rm܏|錰3c3?XVA1 [mm @ge [dhi >,i gEYhN6cc6F׏<ߑm6omm)O{7ש:sٿjw꟏?{tzwe@ h /1A#o6ocz`ZXah9h 2-~F&/4N3 X-lm:+mG<66om6omFsowN׎jqSx뱽O~?U.nm?v^"eeT` KFvFm6m|eFE0%@, LGш44]b!QbB׍X{|mmmxnqbcs:ޟ<9?꟎kOycmA6$Y1#*5صJFF<ۅGghG4_b ҂>l\bw l1neFШno66ooo^6'wr9=~7_|s|stz#cn}o3ab 2X#EPn +;\`g j{Go7Ne(Zj걶mGJK4T8|X*-CCT, ݀OY8Qb-m?om<ۜo7ǛmF=yO|?r>osYQ3 2c}@gXx0RW0ar<,0)V m<,'#p T!m"<ۖ>w! ZiiZHmmmǛw?~qmmx㟷S/xt:u}]O#.&I01#-[uY8bOc )5,+'a@(KXB^#v> m 'KDZHxlycmŚ,GfO <,Y TH A])#mc}6ۜmmm5c߯l7s?O9}??z[#d<,4}NJVsY f (t = |z;4T2pC cy!Md|>,̀[b>[4XAO1!p2,Ū| 5uch{0mF}?My|o?6H͸,mmɎሌogdn[7mxnqo^ol@|0=`4=ޝ0|0=.0=az ~`4%|0pz>oN@|0=4 Apz>ol Apz>oO| Apz>ox@|LC`C`<,_CxX`7v ox Ax韁8݀| Ax,x,xwhz xol A`pz>ol Apz>oNl Ax0pXo0pX0pXo0pX0pX̓`8=`7 Apz>ol@|0=az {`80pXm`8=`7< Apz>ol@|0=az [`80pXm`8=`7 Apz>ox@|0=az om6om6om6om6om6om6om6om6om6om6om6ZG|10~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcmc0vC71@? m(c\1w5tǟ/lcNmǛlcNc }V1ޭO w j7 >cmǿڸ/1pzzC jV/b fbn fbpڼ^0ǥmͺ\1bmV_zcC~bUx5vaF_56GdcUK~1ޫہ=?j1|1Wbm j6Cmm^3; jmmpJt3o4 ux-=/1߆6cWRz]1 m፿ƒO:1Wo5z a j5ywkZA5y|1ۥ5ya5[t @h _}}Ƨcy{ k6f}5zCmx< ? kv,1cO"ww"}15yxc]| m?53}߆6ߘc]Qٝ1 mw m&m6۷}}kxc]gecmc]mdpvl1m}} kxnNokdv5t}}ݽ_}dr3Cfn5ya1ۥ@5[zcU}ܮw kmޯo1}}} kx5z;c] c¡`dem}ޯ%Ѱƻo,1ݒm2&m1}}}} /m;1M6w}Ưիa1a d1f}zavXcm km۵ jm߆6 }Q}}=^C]f k j6 yoj5gܣ%<9f~mW}V] c/wqo5[zݻ^ƫo/፶}}}}ƧWv3v:޿m}}_}z=w_߆5[ypƣn5zIpcVk*鷗o1w}}:_L11mžoCm1۩1dpM6LV Wo? j6HcUņ6l_1/)61߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~oZĘ40~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm_ t۩75y tӆ>۩cO: o1Hc1oOu??m53C }c^ghc1F_gzn[pcCxc~cmWcͺM/@ƣoOcU1ޗpxKy<l11߆7e>᎛z|1ܯ7>Ͼ፶1kXcUomf鿣(:mxcUvaV+5[yaVPcmǛ} m᏶ y>o}06쿆5[y_x$}Vh<7፷7}ޞ}}߾Nޏl1ۅ6cmm5yamC/;፶M}_ݧ~}}}v|1 m፶1m}NOƻnnmݨc]c6±``ev5ycUj kmcUW}}Ʒn]v_}߿5qo5yݷ|1!{m~ j6mcm}7}}scm}}wwiݫ፶}}}w{w{t1x$߾1[a0a{vNFcU;opM6}}z~ j jƫn'O_5yxxxaOO:mtco5cU1oO}xg1}O=AB﷠c}}}}f+sAf(1Cmo}}}nƳo+v_>|1 m፶CmϾc[71|^wnmƒmw}|1ۊ  k6;\1c6±``f}}}1/ mƳo,1{}}} j5y~}v; jݷ{ k6i7|1oW}xCǀ%1ݭ}}}}^oG}y>5ݿ~}~{}}}|1Mϸ۵~s{z}}x$ƫգqv }}~}ƦA j>|1 j }፶ jǛ5[z[8᏿;}c?1 tmҾm mƣo+ƣo@cQzom= j6/ xcޟO/_޿ݯr؃?wK5?1aX03v1C{Nݧ~፶һ_1w+1ox&{ӵCvK}1TcƫoOァ j6\;0᎛u?1/V/W5[tm+1cm6cm_ j_/q\1r1[l*Fƫo/z፶cm}zOWƫo//=:^c_5zc_5[u?\,_6u y:mV_F_ j65xcU1ۥ1ۥ1u jƫo@}cUon፶ox?V޾ jk_W1\2~;ӗEft j]F? bTF?r1?3ܮ[ocW鸿u!]?t'"[6!WI1}v7h1_}1a(d?z[5;_<'}|1UcOMMcU1m[@Z} kfW>cm1ƳnowcY k' bF11;l) 8cY(33 wc]/s 5]_}4cY uz{ϵ~ onխ:ƫ5_zo>igcQͺ\11]X.<2.g9(.  (2E@&pd&^NrxdAxdAxdAd'Ad'Ad'Ad'AdH <(2 ( L)L%0 (2 $xdJadJadAxdAxdAd'Ad'Ad# 2[ L2 L2 (2S|2La1s pr6w m0>~o~oomcm6~omcm6r6۸cm6w o7፶ycm7yc o?፼1߆7/ m~naooc m~o1Cmcm6۔1߆6۸cmCm m፷1 m1n፶1߆7~oco? m1߆6ǟ{mcm m1cm6ۘcm mp~a1߆6~om1ma1Cm17~omcm6~o\1߆6~omcmp vd m፶1߆6n፶So m፶omcm m0w m0~om1߆6~o0u1Qcm6s m`co? m፶1ݡpomcm m%p#.gc#m1m1ۊa1߆6߸co m፶1mcm6c} mpr6۸co鵀[xfPr6cm m0~om1܁ox% K5z6~;m8sC6m1߆6ۘcm }፶omc mw m፶om1߆6ۘcm mm6UQ_n;{0ƫKmcm m፶1߆7۰1 y~oF@~W]D\#6cm6ې1{1mcm m፶1߆6۸cm6r6cm m (ξJ 9p- z2} tf.6>۪om1߆6۸cy\Kms m᏶o}pn፶omc o፾1߆6~om1߆7cm m፶na6jt1e pcpǧ5rHc0ۊomc6ۘcmpCp^|1ۥ6ۛ^ wdV@~cގ^s mhcmomcm m፶1߆6w m~@ }0} }Y bU ї j8kGXfADEc)Hco>6s o7ps}m1߆5[t}p&A.mW\1yPMmcm1߆6~omcm m፶1߆6ۘc} m፶1߆6~Rb7ǡpU ߆6Ǜ| m~o[˻0 m} m7d፶cm t፶1Cm1m1߆6w m0~om1߆6v6cm m0 ^[X3a cmm1Cm1 o7f1mom1 mƣ,1g፶1߆6~{mco m0~o1Cmcm o0Ǜ~om1`e1mn፶፶na1߸ c5yav[\1߆6ۘcm6|11\Zʆ:momco? m፶1߆6~om16~o m0~oy mcm6۴1ޡ1߆6~cmp#ʼ= o?፼1y{0fX^U>mcm m፶ocm mƻoP~omcm6ۘcm yhc6Ǜ~m~om1Cm1߆6~gP6? oo፼1߆6ƫnnavcPQc+ fէcϹ'8cm m፷17~omc]11߆6| m~0co m፶1߆6| mcm m፶o1߸D1߼= kcm y፶1߆> g6| mՀGƼ1 CV4p0cc?AB V(cϿ mX1߆7zfj`{g3 xvޕ03xj3 0g<` j6a6} Y@ ᄂΆ"loB¸`Y,Ym0pΡb[3Զ *%g"Jw }? ~_ӡ 2_Rb,73GoU j |1߆9 X <1"i;5IL,XcLt1) c~ +1XcU pY 6YAap1gL3c1߆7q$2VY0YP0 4aXd" dɕ)JE3 H0a``SL  hb+gLo HCcVi2 *H> q ;oacMxQ d#xۃSCC1!r hш3X388a@ūP2\Y`Cd22ᢌ0 3ge̼3 t31p~oas6g.#P d7l/*C0]Vfb-R0 !fJ ,Fat2pe1ݻ,AOx= afֆ5Xg ы3(vWJ +c]nڦ(1p63&X_cWgӰ`\}|3/ 0f ~o Cl/fp_16bcڙ 4Cm3 !1m賰 o1C0`. @2(c01*` 0g=\#>ৃ< <3\p#V 0ƻ b" 3'_Q9PZt1߆?60awٖ,/co ݅2ٟ_ m\cm m a = m a a{ ^6ƻoP .l.6`aaxg mUL5 ,(M2!PfCgHXYX1@V4 nl)@3 e0 g[_763~6gVx J446jmd63 1e3 3e>Xf Pƫo 2bS 8/fu5zʘs0 831߆6gl.x.0aq~2C< nUφA2gp[`

    ` 1)  lgULC6X@ |2/ ` |A؍ C0| Ba>XbAy{F31Cl2̳> oX3lZ2ٙ3/ oz3[ B?CV l2 _&dFzCxąbBl cxcma. Ʉ eg)m mմI&)80 CNxB u*ѝC[zfB\e FyƼj  `JV$)`!<3<`1i7.-B3 D,nfrb &b ld7e3;pϠ. B2( 7 !01:>Anf0D0Z`-> c0*$ |6[ZdbP&RP-51P<2Zp-l<3)OajH dg :$+8> }0[;;1n#y m~+}:>/ Nj$1n*<#`CD`- $%`$dC1(2w m᎒6, OZܠƒ*Ӌ (d-ـ@-E 1;߀Hg wH.\ƫZ8U{|11\=(!&M5AƷcm4| frPv7=cSӥ5~K  c n᎛um1cy mƫoP~nfޡomcm5z7ǟ~ۡ61momc}6aomc0 y፶1߆6ƻo,1߆6۸cm yVXcm m| y፶1ݡ1Cmcm m፶1߆6s m፶1߆>~m kxc m61߆>~omcm mw1߆6~o_5ya1߆6w m፶omcy m፶1mc;>cm m፷16~om1 kmcm6cm m፶16c0 m1߆6~om1ۅ6۸cm7 mp~oycm m፶1߆7Ǜ~o1߆6~{g1߆5zXcm mr6cm m፶1Cm1mcm m፶1߸/pC km16~omcm6ޘcmmcm m~oycm m፶1߼? mp~o}1 m(cm m፶oƻnnaoom1߆6۔1݁m6_'~ma1\ mC y~oxco6ۘcm j m8cUco? m| mhcm6cmavL1mm1߆6co;m1߼'O5[l[^,1@~፼1m19m56ۘc6~om11m1۪o }፶ k616,1Cm16cmsw5́lXcm mVo mmCO td:m=s5[uaV޼1ߺ1፶namam6۞ mxcm jmcU m>m m፶1ۨna5z6۟*5t~m፷5[z61۪n|11o16moncmmxo,1߆<ۘcm mm᎛loVްw m᏶11mcmm1߆62o,1߆6~mv/?6cmmna1߆6cm6۸cm6c01߆6-mƆ5yao1bƫo@w m0ƫoP o፾1߆6cm m0 fz6w3 e @/ͺXcm፶onpƫo\1߆6 k m፶ovްa߼ƻo/6cm ja j530Mm k7 }፶1߆5z6~om7l1o6o ms:m mmom k m0 fu}xKþ fvta 6na m፶ mxcmav޸c]cm m1mcm61mcm6۾n~鷨cm፶11፶ k mcT11߆611^: v6ocm k፶1m1߆3 ޘcm mcmf_a6 kcm mc21፶1c_gom1ۥo7c]׼-x[o𷃼᎛oApM9lڸ8-O[NA#p[ (n#XXcm jwm7.܉Ûm.a.13 n`'Mts=5[p.ۢ~@1\}<Ʒw9An.<۬1ۥO}U~+ m nva1moOr mMv[m݋:p{}v%ܯUa}|1ܡÝ{ndr7mߪ%a}xc_+}m{'&]]7]ۛqtسWo:iv܋/~6x_s:m5[|afP~na1߆6mm m11!vށnXc]K1d5l j!V';mC6mcmƫo^oo@~ow m0v kذ} km k6޼1߆5[l1d6۟ k66~m m0wm~mmc]K6momƫncmƳoPs mpoc]cm3 m{{o,1߆<ۘcm mm᎛loVްw m᏶11mcmm1߆6e;`}l1߆6~omc]K5t jo mxcm6~o፶n፶oo@~or3 ƻo,1m5[l^mon፶mo1߆6 m፶ooP~naw;1ƻo,1Ƴnnxcm66ۜ1 m፶1Cmcm6c]1ors1፶oXcm!LL1o@Cmcm mvޡ1߆6s;@Ǜ| m1 m} m8cm6ۜ1oHcmƻoP~nc]cm6c0۫yan m1momn}፶5pƻo@~oc2s mc]6s m|11oPmcYcmmmozc?cm mcmco?5[t>ocm o፶1׆6 m0~6 f[za1፶5y}NpK6 kvް~oecmmcmƾ,1߆6cUK8cmgxxxx' f<t ~wc]co=C1^wxcUo +q p  p]<]c_%nM[Kޜr7(竅V j(1߆5[|O;Ns? fz]pGpz}W41ۅ)v76%XVJ(1w߼owܻc2ۋǞ[ k6o};W E^As}vޯ1m'paco>w$ VC1 k6v[ m;}c]}=~5X+M}m+;{ro-]_}mZ*.oww3so]ޜ1_}ƻn}w!@eX%T}cyy9ȞwmnnDrn︆o={|1|18c0 ƻo/}m@ƫocm5uCmcm>~oooP~oP~omcm5ya1vXcm6cUom1!F_6ƫo m0~omc mvޞoPƻo,1Xcm m፶ m፶1߆5ypV.mmmͽCmcm y᏶1!vXcm6cm m6ƻo,1߆66ۿ jذ፶:m5| mV޿ k6 m k m~o}5ts m፶1߆6ƫnocQ5[zm{Xc]| k m<1ۥ6ۗ5|1dƫoP j5[zfXcmCox~mmam{mɆ5[z~mټ{{6m jذ j6m vbP8h ~ q$,@(X(,3\} V2zp7 jmP1m j m፶1߆6ۘcm~ovށ1mcm m፶m65[yA1~ocm j6 m~omcm m፶m፶m6c]omcmmcm mf\1|1ˆ5yxcޞmVޡ1߆6w m$1 m| m፶1m6~oXcmww j6cUcm11mcm k6mm6~omcm6ۿF޿5[z6mƳo,1ͽmm6 jcQSCm1m5[yamom jmmxcm j6V_Vށm m1ۥ1ۥ61nKlaQmmmmmmmmmmmmmmmmmmmmmm n6 7Rf޲0n6 $N0~r) ~>Z1[1?TtX-t~9a;0$Z'Pc``u(1v`0I$`J l 0n6 ;$ADV9f~]AiAcc }]>1`tXr ,Pc``;0$87RfA`H;0$J luX1v`0H*AbcE20 L-H?`-`E3` ~Cl9fuX1v`0HKPc``; ;AHPc``?l9aBߙS'4~9aPc``;0$R#Pc``u(1v`0IИ7Rf+a0Q ~ˇTԺ T ~C V J l +'Pc``u(1v`0I;0$n6 Zĺ T-D  `>3APc``P^@J l H7RfZ " 1 {hRixr@`"-l-P-(PjÐv`0IwP*҃fiAL(r *Շ `oj{ Z-o77= {|6mJɂG|oo*|mƦnFW~G׏v号6{;}6mc}6o}7om6ooGmmommoٿ7ocyF}moF}6m|6om6om6om|6m#m6߿6m6oۼycmF|6oޱmoymm#mmmm7o|o>ohx{ d\oWmhmc}|m66ommmm|卶6nmom6om|>=s+c=lz#\I׍G#o6|mom7o?ϱcm6om6omǛvcmm6n#o6mmmomëą oX:ѵgkHim7mommmmo#mm6mhom6oy6om6om7om}>vHXd8ݑm6mxommmymcoFmmomvF}6nqm|6om6mco{Fm#mmommmmmo?mmomm׍6oom6ۼ{8oݑmm#6mm6mm6om7m6niX\!fPɓ=6ommN6o#mcymms|yܱw6om^̰omQ3ځ}m{y%ً Yl/J̶emo j{a~3l.]r^ཙY׍ن3.a{2fqrw-as0efYFޯ7Abehu76^1 (yfX_̰o?ن3.,/fY|vvfcg/f\ϿV6?m+ ̹3t7_.Lvlav8/u6l.f8]v_6mdmm .f8]gޯml JfٖgcmcAW X3*TT܍Z$m{.},/fX_lm,m]0̳,3o#o? +2m0򰽰|moasfԙHѶyn4}bLXZt&Q2(2t=} p-yA[d$̭cښc 61c% S%BΗ{Urz1U6Ųl@-o#N7&bLY1 jLIt6 F:o1̞ X[ɒ3l-O31ak?ɓ2 ƿo0c h-r0um2\o8^4L-akMWd-aoԩZmGȸZߩR0kRd GH>܈:k60m7# x["7o6ʏvTm}#mm6ۜmxmmy>۲6om66mom6o#6mdmǛ}7mmom6om#mzm6ys6۲6߼mm6mmm|6o{?6o?mmom6om}mm6omoo6m{cmmmomFm6m#m{6ommomGm6ۼmǛo|mmmmmm6om6ommmdmomomFm6oϿ6gdm#y-Pc@`q(1v0K;p%`J h /0n%4 x7nKlAV ĠƁۀ/Pc@`q(1v0K;p%J h -n%4 p7nKlAV ĠƁۀ/Pc@`q(1v0K;p%J h =n%4 X7k@/Pc@h%`J hKlAV ĠƁۀ/Pc@h%`J hKlA`q(1v 7k@+Pc@h%-n%4ցV ĠƁ0K;Z x7k@/Pc@h%`J h ĠƁ0J;Z ~7k@+Pc@h%[`J hA`mq(1v n%4ց_ ĠƁ0J;Z e ĠƁ0K;Z ~7k@+Pc@h%s`J hA`mq(1v n%4ց^Pc@h%`J hKlA`q(1v -n%4ցV ĠƁۀ/Pc@`q(1v0K[J h n%4 ~7nA_ ĠƁۀ+Pc@`灸;p%`J h -n%4 X7n ĠƁۀ+Pc@`q(1v0J;p%[`J h n%4 {KlAV ĠƁۀ/Pc@`q(1v0K;p%J h 9U%4 p*nl `Ɓۀ.Rc@`cm6om6om6om6om6om6om6om6om6om6om6ۜmQ~Dq]iKpfqs> mL S J5!` ق|/ X0v?^J" NA~ƽzto7v><ݏ;mmfޟ;m1۫cYT1cYcm8cYcm6ۿ11۰1߆6m k66Ƴnmͺ mpsp?x+፶omcYSz6۟ j6n5z6wͽ!v޷`ƻo@Ǜͽm?፶5vaV8cYcm6ۘcmͽCmw; 5zfNmԆ6cQV>׆6ƳoXcY1m1[pz|1፶cmcްwcUφ5v6cY k6 cͿfޜ1C6ۼ1ƳoP} m' k6nwۍf޸cmƫnc;~mcmm mncY1cm6 v5z6~ocm Ƴo@ mnaV<1ͽCm፶ mom1۩m፶5z6۳ mm1q ?mcYoƳo@~mƫo@~m麺|1޼1fްƳofް6cmͽ>mo v v7jUo7}[rN8{tq\1;{zp6qw ''owuN]rN[k9xf^>ӃwpN=2uqn}pGV ƣow{y;}w{w~m~ܮua mﺾ}~7i߻+uapN}wv}V{~m}?׺O8#}}y}z8;>ﹾӻ{۶q7Mq_y+]ƳoO^NecXc<1۰1mƳoL11߆6cm m፶m5z~m m1ncmcUcm5[z k66፶momcm6~mfޡoͽ?ͽmFOcͿ vcUͽCm1߆5z6 k6;momu?1۩QLmC'  167h8a`h0pjnn{=Oð20 ,, *p ^M z9pF7[w 7;÷}}vxNFo/on1? j6m m>mnmo1۰1߆6m j6ƫncUT1܁nw? m0~o>CmcmfސƳo[fށ5[z6|cQmn፶oƳoPx^f޸cU5u8c_mVްƫoHcm6ۘcY}cmm5u0sMaƣo j mƫo^ms ;ocU m፶ m?11Cmͽ72} k6m5ucm m cm፶ƳoHcm|Cmcm m1 k6 m፶ƣooVޡ6 m m6mn1 m r6ƫo\1fށ1Fށ3t7 }m jm mcm61oYx;wnx*xW-/2=w n!P` vu8.N'n#y1{qpx xO{G[nQ' swޝܡFޮw'߹w=}7r\6W>|oXO}} k6-w5[z]{oca￾=ono mǍ2s}ޞ}}n[Wǎ۫cQ`cm6VޘcUc m~om1 jnFޡm1o6cUcm mm፶omcQ5[z6cm j j m፶mƫoP| mVށ1Ry x&7=#t /)p`];{t 8y} jfq=C}wmw`v OscQ]}0OϿm^pF޽".m]w%-xwo}:*7pƣ{ NKps; k6\2tOw+ rn 0)]9v7{I?qq?{{q\7vޟܯ{<{WӮ ~}|U1otܯ+?_=w}wwJ￾=xeNaG}sw}ܯensp;˕؜nۙnow639wO'+W}o{uRXXp{TBɎ[AcV=v=\ -iꏏ.T 6ùخpDӲN0r@.WC 67O:*Jqt pk+*`bINCi,>w0a.AW{ A. p8 @#pCx.{WnnAn . WM0]ys7]%Kaf޿f] =oƣoOM}#þ vfޟ uq j6xƫoW%7;{wx\1L-ӂx\07*.%>o0gA]sS7waY<18q{˺ne܉pm*S}s}߾ܫ|?~su~7ao滻؝wp=^ ]1yn߾}|}s%V3>@$8[p>77y9s1na]'ܡzԝ}}᯾ ?}{{5z5[z?61n6mSs|Tpu%G KNۂm`קp(K'9p BN`cwšw@L |W,P1NݭNpQȖ9 ,Ad9  (]R8UE T|}d,2 8㹍[w7np\v-qWr{K vw'y#}G?]ktz1|1Lޟx+qsFޟ;/[5z‚!;m:m; cYwyaF޿0 Ɉ'^p{w7 7{Ӆݏx=prknpWI8\ݷW}pq-wޮ+ﺽ>B% js7+}7~d @ 7;}(8?5|y￾ݿ|1n-P  o'};﻾m\'Ww}qnr7nW~n"}}]ͽc?cQF޿SLpal2T)82` ]a΋'8)[ H 8+h' R6;P#@d5‚~=߀0  `p]2K[> x;qq70 ]w{Vqɻ ppn;,-s7mtxܮۭx;sNx*G[7[0McY7?1w⸆NpWpNn4pN~rNWAwpqʲdaɱ7W7o{9t7[ |1ۊw}}|1}_}=}}q0 ؟9F}~9u}߾}>}[}W! uコorO{؜w߸q]\]}~}߼#xm t۫5zTmDpNhC%2 <: qqXH. mɼ3vp7 )1]q`7ʨf 5rpEh6q4 h0j0J8uX"X;܁{\0K[`K}ݕl3v7x;-t^ٻ۝߼%=Dw+~]^|?pO87;y}}\M?_pƳs L2` +*KpTo{p{vݷ{G;o;q]Z#@1uu}ooND&u}l2`}5z9ܻ6 j6~d[}߾sw6uk m}9}߸۾߸}}xNpN;'}ܮĞWpe}y߼}W<T@pN0 &]V;pT t3 J83&Cd@-©-`,Ϲ® ?0aeG'MpMw0a.;݅`wmq1_ pww$-t6 Nw#xxcYNw`ݻw7A} k66۞Ƴo@~n{VPcmf@~m~ͺc]mm1߆6Ƴn k6 m፶<1 mƫo^mn፶ m8$Ƴn1۩ͽ!fޟo8c]o1ۥ6ۖnmƳncm5z7+ۂͽ8cm6MƳoP~n}21cUS6s m}፶ k6 m፶o1cm1mmoOm66mcmcޟ6cYmpƳoXcYƳo_ƳoO6 v5[zxx[x j6&7[mۄnp>%]CpTop{;q7W nw}3?zuu}}wi kpuuWmmc߻_trwܽcmᛁϻ}׾߾˂+W{￾;c]' 'ݧww}cy kl'+>~ܯ }pU8=>O3t;qWWY cy>/]oo'WM6cU`cmmƳoHcYcmm5z }T1o j mV޿ͽ!fޟ6cYcm m>Ro6~o|11߆6ƳoOƳo@oƫoO5z6~mommƫoPP1cUwp/UgD6:Jߏ 7 p{-;ΆN ,l5ᛍT>L2qsГ8 uW q\M{BnqpMJ0ɘHd;nۻ݇f;on}܂\mwuo{܍o rN%wW#}ܯ5? k6v7MѹܮӀEޜ>OO5uCm}5z6۽}msmoͺ k6vޘcY m0~oͺ11߆6ۿ61mfށc>m5z6۸cm6`͸1۩1m5v޸cmƫncmm|1۰1Cͽswݏͽ8cm6c޿cYcm6ۓ6 j~n@ccm|5z5z6~o1cm16cYcm{v1 moo mm~mcm11Mņ;mfްƳof޿fޟbƫo_1 pcן;y;7#;pup{%=+w?[rAcή ;}I߸ǹ7:5zn~o޼1ۋܡ}-߾g}<1|qk}ۭ}}{O]{w8X c y>;'y<^O'r'N2yn}p„dC}'+ 3OO᎛u j m01mo5zfޟ6;mVޡm1mcm5z61ua} m፶1ͽCm1߆5z5z6 jcYcm6Ƴo@~ofO j m v۩1UD= θ;p{tc:uL2 {uh<3q]\Uʰ%;q\Mv[7mٻ3v17w};'an~߼ܯ;fޮ}x[A{}xݿ￾mmno[5[v6f޿cmmcYS1۫5zaf޼1mcm k6ƳoP~no1ͽmm|1 k6 mps mÆ5q8cUS5ucY1xgx&"m6cYm m8cmcUφ5v6cYfޜ1Cm5z6cm?{V_omo?m6momcm፶ƫoofޡ;cmcmo vmcYmpc޿m k6mƻo\1m+ jF޿t' N ^ [np.];7 f]\)8E®۫wW{?vtmwssw￾5qnCmܷ~]s}s_;nWܷ~1}}g}}W7by>ww5>i;~}cO98;y8=+O't-"NC4ܗ*"  2LɆ:" L#:" )Ձv`0H-YAـ 2e.o VVAـ%XfL)Ձvx$C,,ilX?U `@d( u`D `S `WF u`D H" `E'~ R+bfO} 0̪U4 u`D pS `kC:" Ĭ 01yHd-OCviod\Uv`0INC0S `*" 3F;0$`G6om0~ 5gIWagiM3/Fh-T[ܗ`2+*x`S$Qm*Y\ `HXҽ3ɔd&o,j,f2FA`ŋ4dp˃ 0a h r̙-0c1dxl[-`18 ߀m-qgX%3o2g) x/Ŕaab3ee&L+/TGEד`LX1Flm16f4o}oXooFmkhNZK EAHvNgՠ1Voߘ&W2woh\3%0P,7iP* }< : Uƾdţ }cj7-|dсV,#dX0YX_Eںd̞ޙ]_0g0majV-t-mr0[o[63^e?va*aXd煿,(J(P i=9** 'ZO@,Zjbl-T-LދgΙo|mdzmmGS+N0Z[/ mkE|f?mti2i\Fop`A[t  y֋WókVt!M@[H-`l- Lh8Fi}gc `oF!o y C-s `Ιovnk0ͯ:h[ Tl-o!>#k?9Mrp Zߛa,ݑom8om77nq#mm>mommҭy|m{6o66o6nqymy{}}om6om6om6mmmmmmmǛ}mro|mmmmmmmmommmo?mGmmmomomm|yv6om}mmcm6oyO*v>ymm6om6oͿ?o}66o6om6om7om6om>omǴֱy8om6om7moy6oͿ6om6om7o߿7om6om6om6c$cs<&eR&NLD)K2X}Ǜmmmmm7ommmmmmmmmm}cmEΣ*rf)e,b+̙Xmmom6oo6om6om6om6om6?6om6om>o6om6o6mǛmmmyo|m\Yڠ,anѨ~Fz}>o#m>om6}om6o}FmmcmFmmmym6o7}mmac(6|{mmom=m=o7o}F}6mom}cm|ycy6FY6wou?^L,Hl1endѝoxo# SL38ST+Do,-PؗGLTd,DLoTNJV 3 y6  POzcYYjBLN"sL/ vFۏ{f-;0Bݭ"a h`HW93 aBȖ0cmg[٠,b8djT,V1]~L,DL1{C){t80b0P,LXl S RgL0FeYm-S̐+¶źXDXgБt$EYCTRSd񖱍sj4sӆbF7hx TYd`bdŻƫn wEC&kU2HX)H-͍wL7F=kSVsjx[f{'3Kd2bňs2Ln ,[طhojzV)b-1ŷd%+ako˳3}ȵp~6{X 0JZ ww@3Y3V{No?$~۫*2hWe5KXoXQgnA*ZǾmY2,n`7h,oYqg[P b?y,^$;vvŭgm6۲V,xX䗋_#m-_|ac-Yl#3_~O3[F܍퍶+%`5o|y,lY)X%1ocmKĂͣu =>vkb#m#m0JV_[gZ8lY,Y-l[~Jŏ ŭ\ݒkb-`12vŸ[[oy'+8nk5yvc`3[mō mm,,%1k`6X8bPmA<1:-CzePk:ϨY,Y!^̼6S~kb;f1cc+2ٗlXز-ZmōŮEnȵN6ŀΰ0-ecjOL)yy#6ʈU5).@lb6 3V27.{.ʰ qGM>3H @l sYv v,fŽ:yQbbϞX ܙRqȳ@{T2mͤɘ I"ܘ|m-zEbg-ų,f(x͖Le-1,vjfW{WeFZеOL.@ L- jYPk pę!jFX 0 y|-HZ[|F5}`.@_&)ZƿБY˰38VTLao:1Z R<WL*H[٬3?2t-/.17ɅBalm锌ƽwgson[yrHuU NkXZBLʵ:{;7lԖ\-Hak t5kXc[?$-L->َؘoƶXm6tR#}#m72&<7ƫJTmxomoem#yGm67j폶>y䍷6om6omF}6ocmFmmǻmmmmmm#m=yoymmcmF}o7xomm|7ommom6oy6omv>om7oo?66ommomm6om6om6om6om6om6om66om?o6om6om6m6m6om7m6om6om=6omǟ|mǛmmmmͿ6om6Ϳ6om6om6m6{m6mƳoP}ݾmomcͿ vc j m፶ }፶oƫo@5z611o\1o\1፶fޡf޸cU j6 m| m፶ƫoP~ow6Mm1፶6;m6omcmͽCm] m1m<1mcm mcm mi፶6ƣoL1᎛zmcmmn j jͽm m1߆6666mcm j6cY m~mo፶pO t鷨cm611ۓ j m5z6fޡ1m1a|፶o5zVޡx(cm;oH^ۃn7oݷwzݷovN\OpNWmp }?nۥs'iNnۍt޷W mWW Yx}wIt3�:uu} 5acw}~mwi{}sC6. 21߾﻾m_}q]_}rD3fnnw}O6wzw;o>+Ww;s7v mNkt}zWIIWKz}}}yNw9}庻]{9wy>OpU>!v'd=}'dXLw ==srWpۅ\)V7^_xK^]F7;/7mﺻ庺7;û};O7O@a܃t}wuy x[F7}s_}oN{m+;ܮ0̞o7q}Ov^nno›z\7xU[pqsŹ\MWpۆU®v7W_w}޿toW+{u }}-t_}}<|J'_wswW}wu~w}}.}w~>W}^K/O6ۛ8:y'}vO_w}OŠ@ӝyon}}> y:1oGu V޿YPHwMppwrn+W p\*Ww}W]_ywww]߼oMѼ%;svnw; v-v~}M[sシ{-r߹_r';o ܂]5}wo}o[O`xszsWpkC*.pqY=[7 n7p{_W+kǡ`~ bv㺺:Oww=8߾?%A^{}u_g?}˺oOk~\%A;]￯^ Nwrwzu/};o'j}?u~v߸/}';{? jޯ'N/YDp7 {pnwn+W· U\*on}w߼庺}!zo }ý-y9+r xKsp+~z}Ѻ v/xwܯs7}wuy}'?or7n!xpwÞێV7w{q3tnn+M7 w n*[ 8"pwsNƮy:n{:y:oݷ_}wjopcW7o}Ƴn#}o'}-CC'{}[}o7}s_v}y}ݗNO*}O ȸs}+8=^^Or+'+'Wf޿ZK<@0~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm&w N0ͷ5s{wi ܮW+\5\5¿Ꮏu]_ vw+o}3﻾w }1_s`%o^;v𗄼;]_x.M^oyc_}€r;oδo }W}ͽۖ}}1o_fޛmo;m1o_ k6 m m፶m11fޡ1afށ m፶mcm;m1cmcm mcm5z65z;m1oz k6 m፶moƫo@6:m;m5[z1csn1ocY j m| m፶ƳoP~oOOoavޜ1~mz፶16~o7-ocmzxcm6~o~፶6ƫoL1᎛zmcmmn k6 k66om᎛zmcmm፶1߆5zmcYo 0ƣ{PǛ|:m t1C}5[qxcYvށc]cmm'Fᡎ|7cmCƫo\11f޿1}.:}^p{ wۍo|1x{F *G to7 &`ɺ ~8/}Wݓiq6?wMr'[y\a#w}Ǿ}'߾r},}}ƫoW߾]}M}cms'w}ƳoGwu~WG+/xg}$O㺾;^}N}buېdr'r+?wB"tO߻1mcY vͽm1ͽmf|ƫo_fށ11፶mcޞRRm|1cYcm mM1oV޿ͽͽmcm m;m m m1om~oCm፶1fށfޘcYcmcUS6 to j m1m t۩ t۩cU}omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶ZXH0~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm.{wT`f24,ubv]o66fWU·E.. pkww^u}W&cQc޿nFFu}7a.\ w;coxK7ƦM--[-}FnA_}#r7;}o~}xKox/}oM6˼~m ƳoMy vV޿5z6cmͽm5[z6 j6~mommo~o1߆653zm> m m፶ mƻoP11mMaFaF޿5z6Cm5z61߆5[z6cmcmpƫo_6ܛü'ƳoNmoomfޡ1߹\c] m11o\1߆6~om{} cm፶5zCmo፶1߆6፶<ƫoL1zmcQ5[z6۟ͽCͽ}፶6cް~ocm mf޿~m> j6;m tm5[rpƳo_CmƻoP፶`VndX1 wY@>7nE®_ut=v7߻fnw_}}s}w %ox#[Fޯ?w3rxKw sr7wm`%ૌo_rrn W`]y>i''cVޟ1x+w'5zOs}p`w?cY k6Fޗ # mmƫoHcUcm6px;oH;;]n- x^o x a;; p`wn7ۇO&6dNzzzrNMI+⹧JXfiw~{ܷ~߸8!NW}}~᎛qpM};y_~ۛ^npm5[z~ܯ9C} o'ݷ'i}q^wzܯ|1o_Q|?[˜PN7p *PʀEsn;}Pdj'|2pj$ |/(*|f⸮p7>ﻻo;on݂ppFjwu]7}wi}%w xKxw݂@n>O x.]O^};}A.w7WxK﹛W}~v7 k<`x;sW;/x;_5z{}øXpqn;xn {n0[ panp\nN7i͸܄'[1כ}zW}vWdo7{}};r;~~o~᎛qow￾߾uv[u}+oy8]}ܷw}x8cްRsr}̟}[}poc?gM\?PMnp/vn{1Ao B< ?rt⸮U¿wW]_qۛpyww[^7w:}t3}w}cm_y%.w0}w܍ɼ/77%/m1W+}}پy oO'.o' s~ ܷݏ+xwv?1*M¿5*;Akr' 7]) ۝w{uq'7:{MPo8WB~}߾ur{wu};wܷwu}wPM`npWwt}W}wu᎛~ww}};y;O;o}}y5{z_O-}ܷ  p`:m؄aemmmmmmmmmmmmmmmmmmmmmmA8fi;0$2쑂*p" 8*Ӂv`0IVF D ?*Ӂv`0HVFF D pU `8fi;0$zt0U `,Nr P{ s1jt; ( D (MU8fi;0$,@'8r Z5 D a%U^څN)P[9 c-K!0PoSAـ!]0ŖL*Ӂv`0HVASv`0HG\ p&8fi;0$0eNPVHĩÐv`0H1Ɉ{2T=Y(8 D PNZp" 0*Ӂv`0IHᇓ!Aـ!LC`'py [KAk;; TD `@NAـ"X Hƕ Q1i@Ov-Pƻдal B-37pENBg keH`7y X*Ӂv`0H8fÐv`0H01 e@Mɭf=YAc&QENFÐv`0HnFÐv`0I8r FÐv`0H8r S `*p gy"Aـ&T;0$o6oowc3s<3yX ǛvTc`3kkC`Wݙ̳?1d` y_!/d©km``'0-;f1|>J% WbLPL2u1;`-igq4-",E y(ɋ_4%<-T c2tL+jӜ`2^{3p l;mFӍ>oFo}#m xNنe~V[oG0!kk; -r-_̘|IW!iL59 ^e.I0ًc`3} X,&>v3gd3,c5 reY X `#oN 9;`5yXk[>f?19X l9qm|moyGmo`33bō%`5;bœc^n'ludhbߕZ_1;~0V^0l+3w6mmm نa$ͰeL3%>,)0EjY`)020[{1Ld&kX}ac[U%0-PKB^pY aLl~S,e;0`ĶDZz+~0Y~e2l6K2;υ`5û`3`ʘft@aaPYb,aaLv c>242-%ΰ-aP/0XRi,+Fբ N|[9L2f36۲6mmmm}o# T8-H-C^l 4Z0ƋWf?dcjG߼ j->F:epY#[{8d_-% Pp  X-j/ i5[|.8gp&Rj#a 5;fѐ .HJ zߴ# U`j`- /[ż3wA23L-Hl]ހӺeK>om6mmmuޱmM#m6o7m?ݿk6?or6oƻXo?momgomFmmm{ݿ>o#7o}F}7om?6om6om7owmyoͿv>om6om6o}66om6o6mmm66oy?om6oy6om6om6o6oo6ommo#m73vcmm6om7o66oo7o7ymm{mmmmmmm}jb,mom67omom?msm6nmmommomygmm?K dşe@R`Zh[km7Ǜm6om=m6omm6#m6om'wuǴ 6m+qgcwǾS  5}) Z#{hL`#j]X9#oEnLRc\c 0Xciebj *UL 1X|ȸ&'>@TĖK Wab`7:Q1FvÔLRdŒ(-Q!u40S0qiCZQ0LD dD^ik`f}dXS7jfTˍ[ K V=[k~H ^}0STC V1śdZml?KT5pZW 2`Z,ڣ0lA*y.,Qagg&xv{z9r,bE[-5+(2c>3d[}?Tãj aJx bf[2ۍA#Ǡ[F*TXT&VT#ݑoX ~"lm_kA#i`Z𵭍mf}-QfHciR@k}C._fmt<0 t72ŸZmwbŖ0k0nѶ|ak^foX|k @ml[cVe.q-1y+2ٗomCA#kb׬o};b LX*x #o|c/$rV-lZovÙ =ycg1X lݻ00K`55xv9lk66,~Cm;f1Vر f_#mйYˍ.~L^ bYY̹|f\-[m1^m&b LX䕀׀kom0)$_7`1$H `kdEA 3%Gtʫ|ogHX xlyl,>b>o<>صY-Xس`5`7c3,5Ve.{H{s Z'r,&fIdG [d+a!J-iP&%ި38dȵ-O6c 6lhb6w2 &#mOؾrf>l0%&,"D[,,LD03[- 9oBBaEgZ -7&$XVɔŨ5,{,L1-w"v} 6R̩@1+x y*0qimOm8o"/ēi2qXj6۲6no.6n6om#m6ommm7Xomcy6ˍm6>ooMsdF|~mo7m}o}mmmmmymm{mmy|yw6~ -@ﱶ#}=om6oym7om6om6om6om=ommo7mmmmom{mǟmm|mmmmmmmmmov6om6om6omm6oy7owfƣo[ƫo[fޗN;mM1߆5[z6cUc5z5z6cU j6 m|cQmcmfށfޡƳo@ m፶1᎛z6cްƣo_Cmcmͽ~ͽ>m1߆6cm mm5[z6z~oo j m፶5zݍ{ƳoHcmVސƣoOmcUcm m፶ k6 mv] m፶1߆6mcm1፶1~moƣo_xcYmm6 vcmcU1߆66cY1׆5zaf޾mo Ÿ፶ j6۸cm5zͽCmcY|ͽ!mo x vx1Ov7Mq=AN Vp pӴwvN'WW+C^7];mwWȝwmzr_r;m8H vwpV~ޝ7￾ou/ vwPƣnGw}}W￸};ӵwz}=n'W4}}}~y<}ItOIuO[5z> j6cUcm m፶1ͽpƳoNm k6?;F޾oƫo@}fޡ11Cm11CmcmZ 5z j j m1ƫoOm<1nmcm m፶m5z6m5[z5[z6~o vۨoFޡF޿m፶11n1o1cmcL1cQS }W@~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm]\1omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶ZyKVZO~bV@Sqң*Q\O+=.,q˂: T~BPcU'A"Pq1K1uTmr0R] GW*x87U-.WwnD݅$ & q܃x*w}w:m}}'^N7MОܮw;o}/ K/ }O7{}}}o7?O+ӕ݃};'v'߽} j5z61xcU1mo vvot31ۋno·ڼnnooo=㽺~ F0ӄnN3pv7ww$nF7+},w;wvNm}tZ;Ww}￾߾፶:60u}}p{o~}￾W}p7}}Oӳ߾■M!? jpX;Q;cQ鷯mƣoPFޡF޿mmƣoO b&߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~om^4<`OO(}:Jq.2Fl?3p=#7?C7Ws6\rٱsٱsV"Prr㕳U%gwr7W 7#r],[ u÷M0M ]n@}ܮnM{}Wv'7Mc}I}﹛~}݄}tܿw}}ùx#x=}}Cx.w }þyyO}}?}vwm o=y>?@Ozp<x_x^o;7-ooo;›7xwopM8*n p -t8ӌޜ[rw p ĞpbpD"pQ7'cuݷ=}ӭȷcVs[mn~d}~m}Ouu}}}.Xs7;Oo{Ow w}Ꮋy>';dy>wߺܯX3  h0:Мm 6ٱslعǸkp?Ds`#pC ͋Hf[c x3 l]N6.%pRy^o7+>݄oO7}m}"y>}};s\W#7NdpƳo_ax ?U ~oüo71ފǼoo7y~W!wwۄv=÷Wq77 mS37}t(a\@ﯾ}};n}j}}+v7}ܮ=}[}]+ }#s]}W[W}W&;OWq_}}~"aUY828P|`sdH l]N6.qWd ͋f],>UQ mppGpW9]7@M-*rCW*]7v  \ p*co:m_sNӝ૶NFv7ib9;\6} v }{ I}rϼO}W`}';v܉xK'ƮNc޿ƫo_x?MpƳo_8cUc޿+ƫo^x_^ j6V޿* j=%bXg;a3x}x;\{t(^qۄpp}cp7vcv7ܓn"}8)/\;OOoN{?{a'6\?_tﻺ}x\X'n;_t}ewww}ݷt'w^W߾W}ᄚV=qn3s'ow}r}`ny5_5[z M>Q\w\*wzX#{7 @!pR] t@nx#vr`.\@7 ~o^-d;n:7MFo'ܯN_7O}ȟ}}}ݧ~ovyOW}ywwv۰Uxw}w}\Wܯy_s}܂rOOOy<_c>c5[zƫo_ͽx&~ mܛ_ t }_፶ ??AF޸c޾<3x?x;^xw oxOp.W6Uv<3 nanp1>}ŽE† ‚ =xw+A$\nۤ tەƷ+w'W5s;m}>wWz}}ܷ}+n8?}ݧo> 7;}sOv};Wޝ'}徯珞yݷ+Ǽnwӹ^o}xCwR Dc '_Q\m(wU:sǿ|;oj`M?[7McoΏaGt]ķep1[jp`Fs᏶᎛z6mA p0B77ۓr7+!}=> N7w/}m};~?}y>w;w߾O߾w}/y>}w`}'F}y>0]ro۠w@N}rܯwrݷ"}v7u};m@hc[g'1o_}}}?8"<'‚K vp\< y?xK7;Wx8X1c`K,%an;øp an縁 q púNr0ݷcpK]Tjy݉}{r z~{[r@ };~o_{ur}+\ s߾1ޑ}C~߾]"oIp*>yss}ߺn'-}︘c޷9;}+y<}}O7m}~N.C6gx"Q\/TqTU0*D1!Hl:}so v׆53u0ƣo/;A/az61n } t1m0BoF77@7wn@݃x!q7`þpB@cGn 7_H!u{ vOܯ'&i1/}}}"}ܷ}cmcy>}ww}}+oE}'ܯ\JO'O}+rr}y>s'ݿ1o_x'c޿ ' rn᎛|GcYp{҃PAop.e)cS7(7o1on0 ,;wan0-[;݅/xwA(;=7nn \onn (XcܕyϺNۍnݷwjrn#o7rm8 9܃>}}W7}߾}-][}~~߾` u}ͻa7}~r7O;P&;7Ӷyyrau7opDݓR|X :s/` _ƣnp֑ruxcP/wv^`}m5l11ݏoo7@m>.@#Pp`? g1nwuqͿsNONW;o'W+t7&~7"}}}sw}ߺ{_}7O}Ws~}}O;}}~{zo} j6f_tcQ0F"G}c?}f޿*8*pUm5zWz|1 t 75r<X<1ފ]oE`ㅂ0w%^fޯ x'x;p`w;T1rOp`wpzf4;#9;wӏtޝ;w;y>9\NӶ&wo]Xf?sNwW߾ww+þ#}}Ko7߆;q} k6sɼuܯ.7+%}zy>n.pA2^﹛y>y7A}~Any \7`ndxܮ︮Mnǿ| m0o}{dw'm;1޼1_sWrW;WI-o?%y9y9\'<'U} mᏼ mM|1W\߹~}᏶c޿:{ j9s }#`K1(-鷬n1xdhq0:} j7pqxqü-;N7{ჹPK ~ wi^wvp-8H\;v :ܛクPg_y:ojܭóO}۱}c}=s݃_}߾!}w߾߾M؝ymľ齺O<]i#0M}y>?}~7wy￿oPƫoOASw m. j6pƮ~3o8pGppM`}x#x#qona.F77 c޽ƝW}sޜWW}M_}0̞Oz߼O}}}v }ܯ:my>N7}oO'}߾}}N۝᎛z_Ƴo^Vmx;w _}.Wv p&?ͽm+w ?L8'pT-5[z<o xü/x{üo rwFa໒}Sppӄ'pq͹=ܯk2p' j@w+ _~"o mw~͹}߾u k6⺾}_}naco9߹_}}ܮcm}} j6>}P4Ϯw}yo'ܮ{;W߹y m+t0?u'}sy9n;7r\{r{q' XammmmmmmmmmmmmmmmmmmmmmmXfUv`0He#U`D 0EVAـ#'U`D 0EVAـ'#U`D 0EVAـ#|*" 8)Ձv`0HɁNN u`D 790)Ձv`0INF#:" )Ձv`0Hd`VAـ#:" )Ձv`0HNF#:" )Ձv`0HNF u`D 7pS `XfF u`D 0S `NF u`D o0S `Xf|;0$`VAـ#HNF u`D 7l;0$`VAـ#|Xf;0$o0S `XfF u`D 0S `Xf;0$o`VAـ#:" )Ձv`0HNFF u`D 0S `d`VAـ#:" ;0$`VAـ&|Xf;0$o`VAـ#:" ;0$`VAـ##:" )Ձv`0H`VAـ&:" )Ձv`0HNF#:" )Ձv`0Hݲ0S `*" " `XfUv`0HNFLÐv`0HXr "Aـ#Ua;0$~r0EVLÐv`0I0EVFÐv`0H$`9fU `Xr "Aـ##Ua;0$`9f6mFmmmm6nH"2|o{6mxϹm}yY~D,SLv6vFsmos?o8m6m7ۼm8ocmFӏ}}ي0`0XL.ooGs}7mmo6ۜmxeX0@E AkݑrFm6ommxoϿ6ommmm",<mޖ,񶣜z\Z@U쥹0 +maWmc58{}- UuH`;`d̲emW若0XWه#m܍퍶6mmmmm8$0X\Q& دT ,ELQ,J+p sP[@0XeB^,jabj @R@C00@cη&") Y&LH0Eg,a@`Xa\ X lZ /Z,bدe VY* ~)M( -j4WEiF`",r9b dXc7",moms6o>om6yY>g,glf4SJ- r) 222 0gҞ13\`^Ɂ+K] +`.-7V &d+-h0`"Ie _A0|hZHɥFH)vLDhFn+Yf@k 0EgE-Hf{+϶aY-Zmo7mmmmmm _`lolf;+)\^X,hoPWS+`|[`6X,X,ho -d\m[+llşlXŬY\[0Ÿ+-6om7om6o}6=ome0f}I^)tr& xv3(Y?-_TZ+|gdvO .  `2L_\[Rx3&$m0,}w8`0 /I0b>fr2와S0)+-oX,MPf0,/,Lؿ@e`3ŷ@jjEY.aKOiY!YM{}6om6om>om8dxgZ)  ~Q juO2-9wLjܡc B`X6L "At2d`ȴiGΰm?P J\,Ƀ ` 94֙tdd 7dů}+ ` e6sl I<2O/]`c3 XiyQ@ZWj/`ߘŧtɝ2cFmomm}6mmmV c!m?o} LZ#ǻ܏_ 셮LZm5?6fF}ϱ1[#7mo|ymmmmmomm|7momm6moF}>6o}>6mmoo7mmǻmowmmmm=om{|mm|6yo|{mymmo?}mmmmmmmmm̉f8mmm?om7?6o??{m6om7oy6oo6om6mmm^0bU3&%$;kv|6om6m6om6oy6ۖ6o^6oo9m6om6om6L~b- }ZYdX4CL^>1#mmmoogmo}ymmmǿmmmmmo7}6qVfS 0 IBlvom6om7Ϳ6om6oy6mm6om#;ӥ`hۿ)nB|FYw6ۜ=[|FY6moT2}/೅u6mO?5opayQ %ೱagۧ)n|m3 H| O&MkX-'6T 1>yv@,7XeP4 eV,,,6-,2ZmBF a7.-nѮ6.,@,bg ||ɋh̀{͋Kb6b`,N1lfVt[umWjR`6+f_Kg ?3b5y1߲Td@ bݥ VTΖqglV I݀7hM5C*xyccǻpQcƗ@k-ܞvW3-8 iwFW[` .Ƀ -83̙of ^[ݻ#m`/ߓMW|nl `1 |Ζ|g2C-o-`ſ+2ٗ +K`̰ fzb6vCQ!f &,-PY ~6f  x l|l0cFsc϶ \Z+ u)2r w|dZ *`εeb czBPϽo|6>V^" ߤW7 7F3,mlVf>`ߗe|g`2)oA!^C1m0){+2 +I,a>1X0\! L۷|+8np `6X{xYسkŬm0 XqѽoC|e2OLgAU4dԱܘ|ɝԌ:kHnEՁ.3h^6 (Ȱaj.<#eSr#kkZWA"p `IcL+g|6 m~Iݦb]pcAi`YS:fyw>\LAۙ-c0Z CZŒ/Q-D-g1f]Hnm #  yvgL0`3?u 4m1f h-S Ιէ|bk40x-9vh*e. Z<7Mf|d^ T,\-*3Js5Fm6a!XZD78f-e {p-uJ:-">yNLxo0  ~tf- -?m5m}ߒ3^6ӏ>폶mǻ|6|mmǿ|6ommmmom=o?6m6mXCoom6>mommmm6o6om6om6oϿom6oo?6om6om6{m6{o6om6ommmmmmmm}mmmmmmmmo|m}mmmo|mo|mmymmm7no9o}m6oo?6om6Ϳ6om6oͿXn;p%=S+`Xn e`@ S+`mXn;p%[`Vۀ+2  {Kl;p%`Vۀ/)mXn;p%[`Vۀ+2  ~)v0JL_> e`@ S+`Xn;p%~`Vۀ+2  ~)v0JL_ e`@ S+`mXn;p%-S+`Xn e`@ S+`LV e`@ -S+`XnKx;p%`Vۀ/2  X)v0KLV e`@ /S+`XnKl;p%`Vۀ/2  X)v0KLV e`@ -S+`XnKx;p%`Vۀ/)mXn;p%-S+`XnKl;p%`Vۀ/2  X)v0KLV e`@ -S+`XnKl;p%Vۀ/2  X)v0KLV e`@ -S+`XnKl;p%`Vۀ/2  X)v0KLV e`@ -S+`"  ~)v0JXnKxv0JXnKl`" b6}6om6om6om6nqmms6om6om6om6om6om6Tq|XcPr6cmom7au?6۟pU1F@pcHcޟm;mo;];7cQP1C} v?u m፶W \%Mtn;{qvo݂W@iɸnpGz6;momcozzs==Nn7;ۦ1ްMaz6 j6:y>cQnvoN۫v}y9C10mCmcm m|z}ۙ؝};w ȞO's~{}mmo? j mx[ƫoP?Vޟ m፶m} tۛmn5[z6x5[z6 7s\(ww; pTn87 n0 7 ۍ6ѹ7Cq7Iĝ_y5;T@\{vݓWNqtmӾ}7~}ݧ}}W4vnW--}';}7ܮw}ݾ+ry>}3}sWݷy}麺O}}ޟxg߿yƫoXcUcm mƫoP/Uk\}WÉ n,}# 5z<yB\ v?n>Nӝy;'r^n$y:_}r/Ӥ݂۠Q(N?}r6}sZ۠wW} j}r?;Ov^ݷ}r?O}Nyv?1}yy-~_}}w;}yO'~cY {}o7m;mP pm7߸0y<%Fct 7 یnq;7!MCp CpamA9Qz{v}wj9swoޟI]\]﻾6߽}1sys߻}Ż_r}Н_O};]utnWܛ'߻}?ܯ?N? vVeXX'@ WǼ \GcQcoO5z6۸cͿ }6'ppUW{xK݃9=.ws7 ] \(nӫWv{s^ޟt_}ݧ/1ot}}(}r]<}vw࣠]tOy9_s\WNmy%~߼_}xv?w ۡܟx?5[zFp1}®8cޯspF7w7w7 nppcx[wxwW* W}q7n "wONƫWw[=?{]}^onۧ=サw g#voW߼}}ܮy_}}ҾvWWw]s}O'C}}}+_}v/yo}y>w}O/;mVT~:uxcͺ1$]cޟ$% .|_}P j68H;',q vn=K鷯56X[9} ڸ{Nӫ鷨c}}~ v׆5z7x.^~𷅺o t}'OW;9'xF޷Oo~}ȯo;;. k6wwq71'pF+wwww7 w nmSpvmǸۻ˺7ӺnMr; vwvoޜOOnn۶qsOܯtww}+}wws}m}}Ҿ7';';w߾>}uN5q]}o|1ӾOO_:o:mW`_}¿+roW}w}s +uuo_}O'LarW-y9y>sy<=㽽:O5z ]sqyOӸcܟ/N 1[m;7 M \7r]9;No].Nw}ss}[wu}}w~nﺺn^'O}}y<ޮivNx%yo/}⼾5zQ? t۫XZ|Lpwo}aFޯ qox&wHw oo$=w4}䜮$⸍v}؟}پۤt\v}Iþ<x&}M}t7ﺼ}}̟'w݅=rw6_&m;m®ms'p{pۃ+w+f"}wwWqn{nw;^Ou{߻w}ﺻ~}}򽽸5q]߾_r}~ ݧ;'y8؝vO'ONy\5ys mp} mv61Pw;mԆ;m>6cm᎛uC1m1}Ԇ61> m?o1Pw>T1mmcm;mm0y1o@ }cmcϹm6۰1cmk_;m m᏶6ߓz6c} t q፾rr ᎟z~{_?ݗx1nO=Cc}፶om鷯::XK_} v𷅻t`Wp.%pw 1oP~{cޡcmMݷwz}{ﭹܯ᎛z?M|1o[}M麾C᏿ }>1}.wy>v]N̞O;m mNdDN{wvw}~>o_{፷ m11?᏶6ݻ vۊcUo1޿ƣn/wm }119k፶w6S w vۛ۫p{.7 ӏo_}\!8D1{W p[ ^Nf蟻ry>ߺcQ}߼rm9wxk}y> >wݧzsiͻ[Ӓy9 y>y_xmǯ1z)5YTP᏷1OWzpm v۩ }WՆ9} yg|z }cy6޷1޸cm;/ƣn1cϽ:m_6۟ܛm1oP}ۇs'qv7t;'A܂57_ v̝'wv'ݓztNO'}w}:{Wiowy>ynWݷw}1+}nwl1z}LH3}Ʀ-7W}jO rNNA"roݕOy_uF޿P_ ،ammmmmmmmmmmmmmmmmmmmmmm" "Aـ!dY `+ D w0EVAـ'U`D ?0EVAـ#U`D 7" `*" 8" `*" Uv`0HXfF;0$`F#U`D 0EVAـ##:" " `Xf;0$oVAـ#:" ;0$VAـ#rNL u`D 7d;0$VAـ##:" )Ձv`0INF u`D 7$;0$`VAـ#vNL u`D 70S `Xf;0$`VAـ##:" )Ձv`0H$`VAـ#:" ;0$`VAـ#|Xf;0$o`VAـ#:" F u`D 0S `$`VAـ#:" 7)Ձv`0HNMF u`D 0S `NF u`D g`S `XfN u`D `S `Xf;0$n0EVAـ#:" 3" `*" "Aـ#Ua;0$o`9fU `* 0EVGFÐv`0HXr FÐv`0HXr U `* =8"Aـ&VCv`0Id`r "LF `+!;0$om6}6nHnom|mmovFs6{mwr>oc#omo?cmoY|o8Ϳ>oo6߼m8ocmFs6o|6nqs6ۼ{#͹#ͻco6oy}}xͿ{mm6oov6ۜmw䍶mmxoǟw>66nmmm#cmG7om|o|mo>om6om>b[;S'o}/ @g-2`+S\΀Úc&< J=1|Ue ڛ[aP`zg  N{xr\Xx,r P`-=,c@YBřzB0) d }mmmmmmFm6omF}o^$1d jX2 A'1\Ɩ/`-X$6"UF@̵@VU +l-|/[`ɐ,ch̲̲̘5(D0,Gʓ c2 /⠲lZ@-`Y 1 iPĒ4 Tɀ`7j yf5AdxZ5X ntZ0Z2Vd !0$LOC _1*#h5ρ$!_" X f{v66oo?7oͿ6om6o6;ōHA!$A!7lCV HlG3K3OYg"+KϦ =d,GՂOj%Y}oA#0 $vV ̨9P`5t  qY .dUAf`[`y{#ՂG̶gN$?K$}A!j!oF}7om6>ommmx{m6},lE]Q2 Tf@g (3klGV 8ь@J6c@Y d|Tɒ`Y 6aYɸ, @-d @Wװ,U 72K$1g`9l'Xزcf_dHeT͗V,, }o/|(b[L*2Pεc8a`H bLȖ[e(6{}mmmmo}{ã Yϱ>8jᄁ:5B-=o*FXd_ @SEA[ZgUL|3mom66m6o߿6oϹv|6m6mmy|m6mo6om6mymmym{6om6m6ommmom+mmmom6om6omF}6om6oomosmmommmmmm݅cbr6F}~mmoogmmomomm6qm{6om6omm6ܺ@YYŲi3M7#mmmԏ7omFv6o66momwmmcm6ߖ6_,%1o mmsmmmǞmmm|6om7y6{Cp}zeoZsG Qگ#mmm#Ͽ6ۜmglXسHD,Ղ*L Jf"0bt%2hL,`-,7)r_ Ħ{LTf $-eP\,K!dyU]F= wR+\v}!g,$%,5عip0 dd:,)xjP,4X{Lv6zW fYNXY%0T̅cYm.BIȼ)EhXY0T'Nom7ߓa`$X #!iw0g[b&` J1KM,@oj?Nr̀Z,#Fu(`$(b LXZp- SXgliU2cL-"Ƀ0_HA7cM|f|`TY$ WmT^d!Q[0d Q`3 Y/`YY%Jnѭ_i<`[͋7n2,³b4t= Sb ]3j6ۼmmB7$08oab,vCdw, md, f!2Jo#m MSpb %oi{[3h$0;+2ح,9dbB hYhoU^zx o~N68d[`b~ō9lWomG2Qd[f)]L/DY1mm6ه}vFxb&$~6YHC 5c:4\ܘ5` 7maHnرafIX |(milmOA#efsl iQ+2 m츳&,lV֍l ox ?lN+ ?bY{"E"1$}y$ꬨ79vQh$-P<䍶6omv: gp H2`h  hP mc~|-Pi~|Xl"  ,ZoFXTūX[0Z4 y*,s- `p6 u3*6x hc``|-AB +b*\-ڸe2t9{ oi0b $̫of=}7̌@[|jLNP-]r5)Dž_Ɗ>Wv~ePJ0gw>o6o>>{ݿ76om6om6om>>om6o?66پFm6om6oͿommmmmmmmm|6mmmmmmyǛmmo|mm}}mmmmmmmmcm7m6om6om6om6oy=m6ommdm>~mmm6o|ymmo|yәPDVۀ/E`@ =S+`XnK;p%`Vۀ/2  X)v0KLV e`@ )v0JL^R2  X)v0KLV e`@ 9S+`XnKl;p%`Vۀ/2  X)v0KLV e`@ -S+`XnKl;p%`Vۀ/2  x)v0K[`Vۀ.2  yKl;p%`Vۀ/imXn;p%[Vۀ+2  ~)v0JL_ e`@ S+`mXn;p%[`Vۀ/2  ~)v0JL_ e`@ S+`mXn;p%[Vۀ+2  ~)v0JL^R2  X)v0K[`Vۀ+2  yKl;p%`Vۀ/2  X)v0KLV e`@ )v0JL_ e`@ S+`mXn;p%[`Vۀ+2  ~)v0JL_6 e`@ S+`mXn;p%[`Vۀ/2  ~)v0KXnKl;p%`_;p%`_6Àv0JXp ~1#ommmmmmmmm8om6ommmmmmmmmmmmZNT6mcޡns;} m፶n፶ocm?:m፶T11;m;mn o0ǟw v1߆o;m t }c6c_t}cvOtO+ž''EttDy>c޸c߆6ƣNmVm1߆>~o1 j cm m1611߆6cU k6 m፶mom1߆6Im6۲W;m5z61߆5[zVޡVށ j8cQyomcQ j m1oO| w j6 m!Vށ17c1߆65zM wƣo_ j66 w1oPm mFOmcmZ` tm;n_pYNfynGq#pMu|1CsNӧtc޿_~ﺻ}>n{7"y=m6}'WFOOwi}>Oܟzm፶cQ6፶mbx1ޡsp8_q aFޟ፶;m:W mMy8Xg p8=''giOv'BND:Ӆ ۃ }wޞm{t+}w"}wW~2۟}߾}y]^]O6/s }xgONWt3P3[᎛z5qpw:mmoƣoOF޿᎛z1F޾m[I<\ ޝcs;q^o;F{tn;n n67Iwwwv}_}y7_1oP~;3y>_}o;{y>B}7y>wuz}}W'{}y;_ %“ <bp8]É'8N;+:'D9 OvpDa;7߻nwq^[N-ܯ/߾u۾u~uw}y;o{<ޞ~O'mBtOW$<3;_w}ȟ}y?{}MIx}Cm vm[XXs\CqFޮ nw۰F;pbt}}}u}rNWWWF޷;Ct}I}oo^=}zy>';O'۫}}wrr}~ j~ N(vNp8W'4iSW8̜'d' 9'®$9^ c wx_}'+>?}ޞ݉}'+tt߾?xgO''UO{}y>O'+Nړ3/'}y?v'-}_}p_\C|TOwt}麺ww}__]zuOOWitzuzw}z?vWB'1znӵ_x[[żzvO'i=;OOOOWОOO[W"rmݞ7{u j⺿q w}>>9u᏶z>1Wt}ƣoOx[ot7K{:Oop v^ vۓ1ӽ11cYͽ|1xcmcYƣnNocm k6ͽCmcm[cYcY1ӆ5u<11m mp m5zCmc]׆6ͽƫoOo_͸[_t1nnu+׺]]}t?FܗO1޿cU5z6~omcmcY|1ͽCmcmCͽ8cYcm mƳo@~omcm11፶1 k6 k6 mcYVޟƳo@CmƳoHcYcm mFްƫo_5[zcU j6<ƣn'P¡fޗ?c޿ƳoPf޿ k6Ƴo_ j፶f޾8cm፶1 k6 m፶ƳoOƳoXcY kcYcm1ۊn፶o?5zCmcܟ6ͽrncU mIfPw_W[Y_ur-s]nWIuWIAruuz7pƧzo}}k{{ut'~\11cm6~om5zuut/mmmom1׆5z6 k6 m፶omVޡ mf޸cYcYcmͽ? j5z6mo5zCͽmcm j65[z1?cQP1߆5q? X|aammmmmmmmmmmmmmmmmmmmmmNF u`D N#:"N u`D )Ձvx$o`VA`XgNF tD 8)Ӂvx$NA`8gM䑂8gF D i;< 0U 0HV#Zp"i;< 0U 0HV#Zp"i;< 0U 0HV#Zp"F D *Ӂvx$n0U 0HV#'Zp"i;< }0U 0HV#rHV#Zp"F D *Ӂvx$o`NA`8gFF D *Ӂvx$o`NA`8gFF D *Ӂvx$o`NA`8gFF D *Ӂvx$o`NA`8gF#Zp"i;< oy*Ӂvx$`NA`?8gL D 8*Ӂvx$`NA`8gF D *Ӂvx$`NA`V#Zp"F D *Ӂvx$n0U 0HV&#Zp"i;< 7`U `8fj;0$`NAـ##:" )Ӂv`0HNF u`D 79)Ձv`0HNG#:" 0)Ձv`0HNF u`D =0S `Xf;0$`VAـ&}>momsm}8ocmmm6{mw>Ͽ6m8o6Ϲ׍7oo?mmomm6om}6oom#m卶o>ۼ{#Ϲw{m#ycmmymv7~}oo6oms䏶mmm$mx~qr|m6ۜmv66{o|m~Xow}6{}mm<o?6ۜo|}m8omm8nmoϹ#mcmv=om7oc߿6ޜm}mmoFmmomm/K<֋*Xã5m6omcmF}m|`[ʦ1( 29cU7cmF}mm#y6co?6om7ommc#Wǿ}ސi)>T X$5@ՂGj&ψ݅܅|mψJ|7[Xfj Ա3 &vygӋ;K,|W@+}zb6- ymhۭcyGmmm{6oF|6}' 5@q[Aa/AggeZ*B& _cE~ " c5@[BuJ >0Uqm@B3~ ~!;ob J}P@> (Z+Zac@>VIH 7`RY  A6c,V 5eSpKb~`Xiaa+ ~|,afct ޻ yf'UJ"%:X4@s%`Ib3Na}v卶moocm6mmmm}O-$l_~0}@l'0尼 3IW^/I@^y`g H`ՂKf1p$3>` =HUIQb`΀J@Yg'e_ YgEMD|Etb\ &h~q!g#eK5@œ=g`I=g6ommmo7no}moo}P$$C}4 1ȝΘKI1 _31Eo$*\d#NI&;}Iꅑgw$(\fA+'Y\ xJYmHfg7. L_@xt z X$g;e`Im=T[h"k@GՀ'^M >)dgmo?m?6#~o}j$mP$=R> `Ra1!6-[B񒰿L&df>ZdsLr{eBŴ/W&YбF4b-6fYh&mO6N-[`I`& #`jе;g-I5E =k@K_VjbP`aī %cv@$иk $ PH~`&I Tmm(j*6c j| [L i/90 mmo?6om(썶mm}m7o|mm6om6o~mm,mxom6om6o6츲XZs)i '[Z`őmo}6o}~Ǜm6G6mm6+n v<‰`-t܏mooݱܑmmo6oU`|>\ ?v7?}m7~m6ܑo?m6oo2voi?̎s#y{`6n|y$G+|100aj/k(,-h(zb(2/bHnA'EF*|Nɘ"NY%Y֘XXZ[K<,,дY)1k& K2Zababl` 5i6őSג 3`h- +BZ,YȆy,3|W r*մXKz70 l-h]dV Ni&X{2| ,0 9 `$@. يTfՙeB vE' iV{Z !ſx~ߌ4|/#m6o}~v4Ze@ vPo$MGe@@jt%.νCi#.sـl90HUX$)5wF$3$vR0P.̪MB&~ EY3X˥ȰZp-,֠&1^ %@ |C;FoP:XS #[h:I7P,pd$j0ߵI8FIȇu&bw܏66v|[dmvGj"V/N9X.񷼤#ˀ% ;^VV ܓ$6>V-KS11f[3ĕ m,cTԢޘˠ+%*O-ol%j!b|~c DՂOhIm{`'FɍiPZ0hx )Rfm7omm6߼mxnY` ɫ3zT %HLZ*c6:hY1R?oX,!uBݪ3WL$%f!/D6 D=1X*> v5*,.1+4-W`&Lw+}FX2 T<$c'at^$Hnlh-RcYd>6o}bjE*BoHommo?67oHoͻ|mm#67mR>omq=Ǥ$ޙ>Fmy}wor>m8nHn{m|6o#{?mmcmGmmmmmm[g|6۶>oy}mw<}xom6omG}6m#m6ommmm?mmǟݏ>mmmom6om<6omommmmmǿ}}mmx{o6ommmcmmmo|mmymmommmmmmmo|mmoymoǛmm}mm6omo#6omw6mǛw6ۖ?ϑ6om6omm6{Ϳ6om}c;әPS+0JT/2 I;| =U'0JT/ӻRp I;| /0U'0JT/)8oV @ ~*v%N`T+Rp 6 @ X*v%[`N`8o_ @ X*v%{`N`8o_ @ X*v%[`N`8o_ @ X*v%[N`8o_ @ X*v%[`N`8o_ @ X*v%[N`8o_ @ X*v%[`N`8o_ @ x*v%^`N`8o_ @ p*v%[N`8o_ @ X*v%[`N`8o_ @ X*v%[`N`8o_ @ X*v%^`N`8o^Rp I;| -U'0JT/Rp I;| /0U'0JT/Rp I;| *v%N`T+Rp KlI;| U'0KT+Rp KI;| U'0KT+2p Nx;| S'0KL.2p  ~1om6ommmmmm8om6ommmmmmmmmmmmQ|H0~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcmc޿m'K1o_WKtNz]'Jmo=:7~tuvnņ5z mV޿-3>>^wOtz;v'Jm j6 v۟w}tONf:]wIrOwݖ-OWI k6 mm5zCmcm mc]mV޿m5zc] ko,11፶ƫo11ݻ᎛s m11߆6ƻoP_f޿n mƳn,1v޻ k mofޡ v۟cY cmmo~moƳo_1oPmmt.omv?p}\1} mcQ?fܟ6ݫcmn^'Iv+ӤoN]':Ntzsu k6fo66ۿzv[v'bBso.鷯~]v;^ƫoWvt:W:'KtW m:m5smoCmcmcY1ͽCmcm5z~om11߆6cͿ m5[zcQWVޡ mf޸cYcYcmͽm1ӆ5[zC5[z k6 mcU1 k6 m~ocmmV޿1۪o]mcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߿RPWN]?zu_ں+n;zN5z]]/z}tOu_OOO[z{]+z+Kz~=^^RPݷowݧvݷmڻ}{}׻qmowv j6`U{y;߾'M;}w}{zz{}ѻoo{:m k6avޡ1߆61?5zƻoPCm5zMmomc]cm mpm;mmƻo[ fz<1no k6 mcU鷬15| km፶;mww፶zcmsnۛwӝﻻ v۟w~nܕٺݧ jy;O:m m~ww'|1ۈn߾6߾wwzw}サޟ{?ݼ1oXc޿ k m፶1mcm5z6m j5zmm m፶Tu<@zcޟwݿ;mݷm{y=w{ӻ߆;mm}n]wzv񽽽]iv>}vvwt;{N}{cYm k m፶1fޟm1׆5z6mo1:mƳoP~oCmwcm;m1oƳo_5z11scYcmMacYƻo\1oc mcmwvwwmm޷]vpƫn/1oXcmw k6] mvۻӾw_1o_5z65|፶6 v۝o;vk{wvݷnw}c;mϺ^O}aw+ ƫn/1oXcmo͸޾o{{wvǾ[ o7}t۽ݽ vmom11߆6c1mcQcQT1߆6۟U@ vmvxv'ozwzvO~cQ{ݾ/sONӰݫ{ vw_wƳoO1cYͽƳoP~o v1?mfޡ jwf?cm׻{} I1ۋp͸~o_cYcm mƳo@~oW j5zmm m፶Vi@ v66 k6?mcYcm1 m;m}ۻn}m繷;pw6ݷ6vlv{vݻuw+ۻw{1ۊۻ}cYwt_wo۽}x}w~wxwgn~~{e11߆6cYcm m v۫5[z j6 j66sdaImmmmmmmmmmmmmmmmmmmmmmn 0H!0n 0I##P"@;< wd@;< pn 0H#P"F ԠD 7Rvx$`JA`u(gFN ԠD 7Rvx$o`JA`u(gFF ԠD 7Rvx$`JA`u(gGF ԠD 7Rvx$`JA`u(gFF ԠD 7Rvx$o`JA` 0HRvx$`ũ@;< 70bԠD 1jP"(gF Z'#-JA` 0H$`ũ@;< 0bԠD 1jP"(gFF Z#-JA` 0HRvx$o`ũ@;< 0bԠD 1jP"(gFF Z#-JA` 0HRvx$oũ@;< 0bԠD 3(gF Z#s 0HRvx$o`ũ@;< 0n 0H#P"F ԠD 7Rvx$o0n 0H#}u(gL ԠD 37Rvx$`JA`u(gF ԠD 37Rvx$`JA`u(gF ԠD 7Rvx$`JA`u(gF ԠD 7Rvx$JA`u(gL ԠD 7Rvx$`JA`?u(gF ԠD o>om7om>om=|6}#m6ow}mm}o|mǟyyo6om6o6ommomcm6om6ommm6om6mo|mmmom}m}}mǻ|}mmmmmm#om6m6m6om6m6m6ommm7ym6ۼmKoYmomϱmmo|m}m}6mmmo,2M&@5gQ,=mm}mm8n^~b`D!Dv cmF܍6}oyom6om6o>om7{{yaaF< d,m+zB{cmDnm ?coN߁'G-($dDj;8, >#,| YoǛm?{mocmm}mu K| & Yp:1)E($O'` ,/1KoA [Tr )2"@@b2&*0JX2& \yD z#IX0,$A(06)Ȥ`-d-1 v`1(-H~ e0Z2YT1e`,&7kX\F 'Me)N-cK`-T| YfS`X>W,N$$b4ɌZPU0$ZPH /m:<3+O!/ h$"~ 11iس5`}{?5`pF0] 1T'SI  $$. ~qz\L 8$dIA# :-P-SA&;H>$fA% y$IӂCL 8$rWIommǛmǛm6ۜm}w>o#m<` V<3+iA%?1'!/L$# 4'Ij$H+:2sgopIlL 6#U!)1'>D|,} Jt 10N(1IiA!K'K XI&%`HiA#5$Iݢa$uk[$4: % 9$5 ($@@G%z >t#y?om6mm6oo6o#m~>ʞ&  *),.,jLd{(~Pp!o ɄRaQdgp-h1/`{IYӌOf:ZHBےm d,%dYLrc<e&8c*Ψ0e>26N-3Z@H $ANK$lOm0 WaHz{>#$l!`{h,A L)Y5ga Oi&>̓*[ɂqiL}ƀZ7dgBcmmǿom6qso}#o6t7 _q>@M~d-xa XX*̙'Y[𵿍L7|,/LwemgLǞd,f>!m7mV aJmg0Zל)#]ơ2S*&[%}omm|_m6om6oo6om6o}}m6omm6?76om6om6om6ommmmmmmmmmΘZqm6oo6oo?6om6om6om6omm6ۜmmmm6oo6o R-j5j[SHSՍ67{m6om6om6m6om6ۜmmmm6m6omL1iόR 2"ZX=T.6om6om6om6om6omFmms6ncm6۲6om}=_Ϡ,ĽJ;o|y۟7yom6oͿO[d^!6Ab@*/kL,mN|bh&LLxU$bZ@a |g2,X3P56m "\m!Qe' )JKT-4 p Gc7xa3q@Y-\-$,\Bkp,PZ cϓ &e0 | fa0Ι\-˴ &ũoj; ' Ɲ4+_XA16y%`X,,n3cm :}`[e"708i>+ Z3i @BFmmmmIl/0 E ׁ'0s'r'B31ias <b5vY Ŕc"R$'8Foi $Y ̱C S?"aǁ%\`%)1 6`I$b@J*YQok`x1 gxnR`oAL m|HY҉ŇEstVdB=4mm6omv -$4 DӂO#=g $v/2 D[ OA'Io6 lKb&gm;i%`Il3213Io'b)J 6F~O1y^J c4LL EyX{J 3y/`Z(fdA,Mk?4Dُ-^?L'%`$4R6wSdX%S do70c83lOb&$"2o}:$j5*"X3hza/ LŰ60g*?om7}mix1N 1HA8iܤC E 3"1 I״ LHH3.'X2 ̶Ha0| XgNI26b&cS1!bI y($c2YbCg1)PI咘:J ,ƅgԘڣ?Ml,[M0$YYe||o5IY$.lD>7;LcA+'Nch 04';Hʾ[$`J 1dW$ɁcmOэQmTEI$:>Q _cycm6o6ᷟ hZ0zB[VeA8~ojBx :=\+h oi _ːnYR𱘉 LЂZ3Y&& {:Ѷ$yt̮n29tVIpyք6 U Mo_Y k?hmp`6fBaZh `špgΦ\( mf5S*0hΑޱmmmmǻ|mmmnmzFmmmm}o}v6<<<ɍ6om7o\q0XS0'22om6om6oͿ6om6om7m6oͿ6oyom6om6om7ommmmmm}ycm6omo|owmm#mw6om6om6m#m6om6om6{my}s6zs6oo6om6o?6om6omFmmmmmmcmFmmmmmmymo|mmm|mmmmmmmmmmmymmmmym|mmmmmmmZ6oXom6o|6ϑmmmymxoy6ۼmP@;| n%0K+P K@;| n%0K+P Kl@;| bĠ@ ~1bP (o_ X+,J`v%`ʼn@;| =bĠ@ X1bP Kl(oV X/,J`%0Kv%Ń K~`;| -boV X?0K@ x1`/,v%`Ń Kl~`;| -boV X?0K@ X1`/,v%`Ń Kl~`;| 1`+,v%_`Ń ~ `݃v b0pց_ X?0K@ ~1`+,v%-boV X?0K-boV X?0K[`Ń ~`m;| bo_ X?0J@ ~1`/,J`m%0Jv%^`ʼn@;| bĠ@ ~1bP (o_ X+,J`m%0Jv%sʼn@;| bĠ@ ~7v%`J`mq(oV Ġ@ ~7v%`J`mq(o\ Ġ@ ~7v%`J`mq(oV Ġ@ ~7v%`J` 6ommmmmm6ommmmmmmmmmmmmmmWc@0~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm m፶1߆6~omcm1P;m m1~᎛uW vƫoL1۩mv(cUVށ︼1oP|6 mcQ8cm?c1׺o^]5z~ݷfmw|1c޿psP7wwWڷ;m t۟oviMwwm~:w66۟fޡ6~mo6cYmcm mc0os፶m1mm k6፶C޸cYcmc޿6Ƴo_ mom mA~oͽCmoacm m11፶6m1m0cz?o Xgxon:3֧<1o_ww~mw O^omwa_v^w=m^ ww>m5zƳoXcYcm m m1߆5z6~oͽCm1m6o1s m᎛፶[@ v۩61n v1?zosƳn6.xc޿M?m:mݷcwxf᎛sݽ}1o_:mzcްs11~oͽCm~m~omm mm፶1mm k6፶ƻo\111o_mcY6ƻom m6~mo}5z6ͽCmocm1c]wiwzF݋}c޿w[umww]pw׻ww{ݾ{vӾφ6?O፶1 k6fްƳoP~o1o^ocm k6 m፶1߆5z6cY6 m1o1xcm፶o6mmoovlmvhd|%|%@trak\tkhd|%|%@@mdia mdhd|%|%u06-hdlrmhlrvide VideoHandlerLminfvmhd,hdlrdhlrurl DataHandler$dinfdref url stblstsdmp4vFFMP@HHmpeg4ResdsD<  @- čHcLavc52.20.1(stts @stss  %1=IUamystscPstsz,o  ,w2hom<`$C-~+! ? rUV5xq BPiv#h`)%F5IKRK:rYN@&D9OFXV RLstco$,C~L[ju-zRʙ>;ժݭ~t 1H6:?E*GJlOW\af9 ԋۉOX@ uT6Z;@CG(J/M OTY]`%|gpOqgzx١ A c!$'_DJ%LNPR@STVY\>_"}b@T:z"IJKWEɗPudtaenc Lavf52.31.02013.com.canonical.certification.checkbox-0.4/data/websites/0000775000175000017500000000000012320541307023557 5ustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/websites/PNG_Color_Image_Ubuntu.png0000777000175000017500000000000012320541306037121 2../images/PNG_Color_Image_Ubuntu.pngustar zygazyga000000000000002013.com.canonical.certification.checkbox-0.4/data/websites/flashtest.html0000664000175000017500000000014612320541306026442 0ustar zygazyga00000000000000

    2013.com.canonical.certification.checkbox-0.4/data/websites/flashvideo.html0000664000175000017500000000011712320541306026567 0ustar zygazyga00000000000000
    2013.com.canonical.certification.checkbox-0.4/data/websites/SWF_Test.as0000664000175000017500000000044012320541306025537 0ustar zygazyga00000000000000// Run: mtasc -swf SWF_Test.swf -main -header 640:480:20 SWF_Test.as class Test { static var app : Test; function Test() { _root.createTextField("tf",0,0,0,640,480); _root.tf.text = "Test"; } static function main(mc) { app = new Test(); } } 2013.com.canonical.certification.checkbox-0.4/data/websites/html5_video.html0000664000175000017500000000052012320541306026660 0ustar zygazyga00000000000000 HTML5 Video Test
    This video will play in a loop and reload every 5 seconds
    2013.com.canonical.certification.checkbox-0.4/data/websites/testindex.html0000664000175000017500000000012212320541306026446 0ustar zygazyga00000000000000
    2013.com.canonical.certification.checkbox-0.4/data/websites/SWF_Test.swf0000664000175000017500000000046712320541306025744 0ustar zygazyga00000000000000CWSxDQn0}qRlPE%)Kn, R$G.uSDu+c|F?BRo8{ݽgћw0cg~Ci2)SdY"7o2WZ5\H1L,_ejzU34URR;dU!JS hp -uF#B{ K'd$7kU`eA\ _ [p㵱v-mBt1xg]CL*!MYt-%^ǺwU #J2013.com.canonical.certification.checkbox-0.4/data/websites/Flash_Video.flv0000664000175000017500000115174312320541306026466 0ustar zygazyga00000000000000FLV  onMetaDataduration@Z1'width@height@ videodatarate@hj framerate@=SUk' videocodecid@filesizeAO E.h  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbc`@B `0^ƒ i_F@`9l P` 4+06 (0Br 0hVP`9k P` 4/A/`aA4Ьr 0hX2 X0^ƒ iY/`aA4а0_f`@B X0^ƒ i`  0 `{ g`@BB@ќ0`!M 0 `{ H0@5 (0@ ` M F`8k Pp 4+@0@5 (8@@q 0hf`8j Pp 44DG/`aA4@q 0hh5 P0^ƒi/`aA4/j`C5 P0^ƒi"`_ 0 @{ k`CDQ` M 0 @z ~h0 5 (8@` M F`8Aj Pp 43h0 5 (8@./p 0hf`8Aj Pp 44\\ A/PaA4͠p 0hh6 P0^ƒiA/PaA4qp/m`AC6 P0^ƒi_( 0 @z m`ACEѴ` M 0 @z ~h0 5 (8@` M F`8Aj Pp 43h0 5 (8@..p 0hf`8Aj Pp 44\\ /`aA4`p 0hh5 P0^ƒi/`aA410/k`C5 P0^ƒibߨ 0 @{ j`CDQ ` M 0 @{ ~P0@5 (8@ ` M F`8k Pp 4+@0@5 (8@q 0hV`8k Pp 4,4//`aA4Ьq 0hXHO3 X0^ƒ iY/`aA4а/f`@B X0^ƒ i`_ 0 `{ e`@BQ@`!M  0 `{ ~ 06 (0B@`!M F0`9l P` 4+06 (0B`r 0hV `9l P` 4+/paA4Ь r 0hW0 h0^ƒ iXA/paA4Яo~ `8(H(oC@p?󇇈<>BCq|||$?'8N2.67qqqﳋ~\L\_bbߧ8DDN"#pӆ~,$47 ?ߧ 8(/鿿HG? pp󅆆>BCrӐ~$$$?g!!!!9 HHHN>>BCq}ll|ccߧ8ȿN.*./q11q_Ӊ~DDLOᡢ"?ߧ 8hhxoBCBN  pPPP_ӁW?󃄄,44?""?8E&**+qqq󋋌~\\dg#c#ߧ8N>BB?rӐ~$$dߧ!!##9 HHBBFGr2|$$$?ߧ!8N6.:3qqqӋ~\T\_bbߧ8DDNpАпӄ~`ߦ__ 8xxDDD"&"'q1qQ_󋋋\ddgccc8GN:>:?qӏ~$$$?ߧ%%##9))IIINJJJKrRRR_ﳒ}d'!!!!9 HHBBBCqӏ~|l|wccߧ8N.../q111?Ӊ~<4D?ᡡߧ 8H8XOAAN>C?@ 8xhxgC?"&"'qQqQ󋋋\d\gccc8N>>>?qӐ~$$$?ߧ!!!!9 HHHNJJJKrRRR_|䤤ߧ%%%%9)))/IIINJJJKr22}||ߧ8FN.../qqqqӊ~LLLOb"b?ߧ  8hhhoBBp000?gC?"!q1?󊋊\\\_"?8N>>>?qӏ~$$ߧ%%%%9)))/IIINJJJKrRRR_Ӓ|䤤g%%%%9)))/IIINJJJKrRRR_|$$$?ߧ!!8GN6667qӋ~\\\_bߧ8hoCCCp󂂂4 >BN"""#q11?󉋊T\\_8F>BBCr󐐐~$d$ߧ%%%%9)))/IIINJJJKrRRROӓ~~%%%?ߧ''))99)I/IIINJJJKrRRR_ﳒ|d$d?g!!!!9 GGN>>>?qӍ~\\\_ߧ8DN""pап󄄄`@.8EE.227q󌌍ltlw!!!!9 HHHNBFBKr2RR_Ӓ~䤤ߧ%%%%9)))'JJJ??NRRRQrӔ~$ߧ%%%%9)))/IIINJJJKr22R_ﳑ~#$?ߧ!!8GGN2267qqqӋ~TLTOb"b?ߧ8hXhoCB!4 8Eſ.2.1q󍍍tt|{!!! 9 )'I?NFJJKrRRR_Ӓ~%ߧ'')(999IGJK?NVVZ[rӔ~%%%?ߧ''))999IOJJJNKrRRR_ﳒ~d$g!!!8 N6667qӌ~TT\_bb_ߧ8xxwBB/|?bb"8Fƿ6665qѱ󏏏~||$$d#%%$9) $dNJJJKrRRӔ~%%%)))(9YiIOKJNVRVSrҒӖ~%%ߧ))))9IIIOIJJNJJRSrRR_Ӓ}$$d?'!!!!8 N66:7qqӋ~\L\Kb"b_8hXh_BBBB>#"bb8G66>=q󏏐d$ߧ%%%$,0|I |4r (@ 0*4x@P+|$ߧ)))(9IIIGJJJ?NRRV[rӕ~%eߧ----9iiioJK?NRRVYrӔ~$%ߧ%%)$K%'%$\w(PyH 4(J!  "Ӓ/󐐐|||{ccc_ߧ8ȿĿN&&&%q00󆅆$$,7""b8ƿ:>6=q󏏏$$$?!!##9))"y )|jd 41/Lz>Ț0 PK|80 J?RRRQrӔ~奥ߧ----9iiioKKKNZZZ[rӖ~奥---,9YIYWJJJ??NRRRQrROI0@  ˎ2\02 3 (? >=>B>AqӍ~~lllk""ߧ8pD:'qqqq󌍌~lllscc 8HHH?BBBCrRRR_ӑ~ʁ2d2d(7B80 ` ecUrӔ~奥ߧ---+9iiyKJNZZVUrҿӖ~奥g--+*9iiigKJ??NVRVQrӔ~~0H@  LeF`AcA $HG?BBBAqӎ~d\d[ߧ,L|2 l/?v(Rf0 J%C/OqV`B44?ƀ M`bXP2O~_H|t p0`Rh0#gJ %DTM˛) XI/1XӣKNA;U?]0j ^%SB@A j_VZ8IgJ˿?+^$R*%q0`U%0_D@V>m xq0 z&%{_?9iiigCP@ Wy J鈀`"Dyp0) PDw,~mH TDq ?)(A ~RRZ~w)\>)90 |`ĺn h 5DD#t @`%i`!a= +>%X|ۆQ6%3%%?a,P2<0HV_M8(_fFX|Hx ~ِ0@V㠀<|\T[,\d\0PWĶ CG *u{? 6`cꇀCCEG@ 8FF66653`bEJ0`2alHJx`~/4|0~ 000| 01~ A\ƲK.Y#/>Ch 41P`|*&7 >QA",G?>h HMp&.e_ X0.80*J */x00HQ :80@Z`~~G,9YyYgG@kQ@ ߀H0{o h00"<@0`|X@0`+~|r$C2 RtP`Q'8[G2, EED|ď`25@5(%O >`;B  tV\ F *M~T"jLPd\q90E@4$7r D%;bJ `O qq%`4PҰ0 5= 9( G}X3@2*P L`a!8ό `†n6H†n6EE{8E Lj\pHBcP0000ԉAx 0?Ow!! V8#(qqSlllo`c!p`c 4p_Ą̉e//4z0 $9))' @ 0Q) @ 0P1!!!(+IXƲF_|hb$`4?C!#.=Al`"= ?W?OKVO %yyu =( 0 ~:Št{ kʈB00ʒ9踘 #ǀ<eià rrӕ~|\H0 p h0?~\8׀@8^ ? Ai_, ZW=?9P2`P'`K[D '=bnWm@>HAu`AjQPg8%XKF2$h !MQ j#6ǪLDQX_.{x$J40`KRZzLģs#B^N  CLLPș'?vI_Ey:`0 _iGB=&a0?!@2{dԌ`a! `A `,W `aC7`Ao3~o 2vDN*4>2.2| > R'  3߀4H!!8hhhg򱡔dsC2qNlllk㣣r\j 01p=d6pElLHȓČ>.4Ztd_4 00D haߦ$$$%p`FJ,7d|ha`4? JlMo00 WJ YҮ'~~^~w/!R~a+A'q1J#bBF /I|c8eI\:T쨌xHRG? #ǀ<ei99hp`+-+,`Ap `~\8$H0 Sx00h<rhx+LFx0?%_,a2{0`8Q88 ӅDGA=3X`A `A +* YYiIis/tKA0@+ !#Č. 𙅰`Dx@A%$3&+BnQ\˜LȐ4OOS_|4 3~}|y0q`82j@B# 8d%=|`I3zHD`a~D x0?qqQ@ 8p00 8J3Y0\)Kd 8H0?#~t!!? V9ё~g8F`bai20bbLO0 i`0 0'䤤߼ ``q!3bx{ 20 eU`Ȳ`h4 oEX(F&$!O30K?, MZZZTr'q00A%1@.!*BG Q"C"J3Ӏ0 &UcЂ~+00H@2 `(2z.&:+H1.)}#_Xp`A0 PI~!:Z^T tUÂ@(`DpaHx$Wȿj,VсY? G8< ু{+'0C(`A 6ːK_,NZ^Z]M@ /T X`Dx@'M.#ANNNRQ<0&A!{ČD(dH gUhdTA]021 $\VO}s@WP g`a~D x0?4`K:F,dldc8* Ӣ40>h2i/#ƒ #/Z)U7ICCqQ󎍍~lllkccc c_!?w``o8p0%z@Q}8$`I@Il|.BV ߻0j%$!1P&dd!PBMi0|o4S84K/g)G>XJA%Iad`0~>,C.PH0.ӂx| &\?1E*_/0 @0wKd5i0rCG߶$b h@B{> (0JT2HԏC2K+q0BXJtف=/0':C_X0 ѥG˿N yX4?ƢH0@V_O":F@0~ ]<.\>4vbh2 *BZ4\Ըe,< ۅ< x08YA==Bd` Ok,+""pEF\ccc"߿8cccP08|}%Ѿ 4 Z~MK0 63-U21U0YL'ЉaQq J[  \$*݁mI}q0`Չ: JII?%)7@&JE, p}"@/~ 0 t'7(@7᱀Ci<(,`{!&x[%pPoC*|p_D߾$ ?7*9iyigJRBA}+?+-_yUxFZ+c3Й2Q0#p?Bg-Wq'_uBX0@?+ F<0`&U^|/ĺ?gKʿ?`~,ßA@?d|?U@|ba W`r=BKKWSү޸ Q74A@_d%T;\0`yꋕ^jIJIQR, +"I7:LSį:`U~t3}<&xmPA.V &xVB}y1Ac􁜃0 j~~qP`OW„!/]WⳢg a0 [.ȘxO#ƒtKUa Q@ x} 0 W7ӿD󈇈4447b|-qqo󌍌lllo H,2`|0 7e)';k//eի3$rM)#2R󒒒~rR}1'*bA/20 ̜9@r|Ut̼!`LwLʟ~P奥---,f \ S/r. ?QBwB`G31/hղam߫.域?/,IV簀 j+%߬%^̊cӖ~$p Pe_nd~~|ߧ)''#IW*4)&?sdldkccc_! 9  'III?JJJIrRRRO󒒒$)))(9IiIgKKK??NZZZYrӖ~~奥ߧ----9iyi˿N^^^]rӗ~~ߧ----9YIYOJJJNRJRKrRRR_Ӓ~ߧ8EEN*&*'q111?Ӊ~D67q󏏍|||$#!!!!9)) IHJJJKrRRR_󒔒%$ߧ))%'9IIIOJINRRNSrӔ~%%%?ߧ))))9II)/JIINNJJKrRR2ӑ~#ߧ8N..&'q111?ӈ~DDD7!ߧ 8H8H?AAAn=8EE2623q󍍌ltdo㣿9 HHHFJFKrRRR_󒒒䤤%%%%9)))/JINJJJKrrRӔ~%$ߧ))''9II9?IIINJJJKrRR2?ӑ~##ߧ8FFN.../q111?Ӊ~DDDGߧ 8hHXOAN p0?hx`y}5< P0Q@¼i 0 @E k|`y}*- ` M 0 @E \[h05 +@ ` M`'j ,W 7h05 +@1qmO X0!o`'j ,W 7bA(a^B4MO X0!oƷ@ H0Q`¼i(a^B4_n`y}7@ H0Q`¼i 0 E p`y}BA `A M 0 E ̌0 4 +@ ` M)' `(Ah ,W 7Ӑ0 4 +@2RN@P X0!o `(Ah ,W 7d(a^B4N@P X0!oI9B @0Q`¼i(a^B4_r`y}9B @0Q`¼i߳%$ 0 E r`y}fJI ` M 0 E ~̔0 4 +@ ` M)' `(Ah ,W 7Ӑ0 4 +@2RN@P X0!o `(Ah ,W 7d(a^B4N@P X0!oI9B @0Q`¼i(a^B4_ْr`y}9B @0Q`¼i߳%$ 0 E r`y}fJI ` M 0 E ~̔0 4 +@ ` M`(i ,W 7Ӏ04 +@2P X0!o`(i ,W 7d#(a^B4NP X0!oǸ@ H0Q`¼i(a^B4_ُp`y}7@ H0Q`¼i߳ 0 E m|`y}f.- ` M 0 @E ~\[h05 +@ ` M`'j (W 7X05 +@1 @O P0!o`'j (W 7b"(a^B4M@O P0!o4< P0Q@¼iA(a^B4_نix`y}3: X0Q@½ i߳  0 `E ft`z@}f  `!M 0 `E  (05 +B `!M@`'l (W 7 06 +Bb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !E9 ,!"h 1?2VMx4?N48WX g ½lVwg{ws;woo-l8V°^}.is~8susǕ͞ 0^;P:Csγ:9Νt3;Ut:t9ӬN:9Ν>t9ө3sYΝg:u|YsRgOg:tS:}sWΞk:=I*[2 ϱk %sǾzxu>z``תksǶgW/?гu>tyu|33:5|X [;#S mgzt^p+gWC3i/&&&!ԁH TB( 4^~w[:{ j3 ›PUhߎ_Q0ΰ#3<ү\CA@?x<}|tsDZQ~<p*eqMA>{ \^hH&./@ }X4@cv(Ġ 2(Jǀ@:79IIYO }lȼl|Bđ40zX4 Ÿ`_Ah4@m=P(HH00" `a`0pp`^ 0ǛB|2g x|Xx~$+6BT9sZ ;Vڰ/<%D5[AO#.=W> V 04`LODmQF63 (4 x!ҹQyzR m?]}3hedRm~/4vx{hY6TdaC23JK_GAо+wˆ>dQ`#4A=)U*P%JJ!){imPf_KЁC)?*0 G9k w :7((`?) đK8ڷATXaLҢ"s(8` t)*sP+W  p~ aLտPO'FphKK<B ⢢ENq#Q#@|2$ 蓄Pͪ H! '2&IB7N+Y[ǍBH?1>7LOGiRGq,~${@bF]Y `a{W &6w1T :8$Dd~M(RP Εt9*'`a 0T3d3`bS/ER`~Q+W() WDrA 7`iRPiđ0Oу,)zo6HRӐ )|l)|5c"ykڃa1<(oj AAFJ|zM &zn>6zD4)w4ᗾ`iۄ>AJft3f^GU{eZ7g*/.| j4X40DsA{zx_0'F!< `=nV^(?~V O+ «?@uqžġj"HGD )\W&8]{3`-gx)>]h`$"8HN c3^ A P I f  3 eӅ u>|SKV%%$ '^L?Rgh01H. އphm>]j)3Rp2_ۀOC{H<܄%+E~?kսCj.nCMІ|<A~C0`a3PHc40X WUch~Đh~.POD ` MG0B߈J 4C`>F~+xQj0}UhG `h0, 塸0`A˶D v\ALE:"1wGܹT:!)X(eH6aÊ)3c }FF^]h04 ANT_F%\Z%+<:H # Xz2@4Oq084AM9E h`h!B^b!(3‡|2xYG\VӽXg2_2 8h1DA AA g00 h*U@Ph@ 0?AS@p%ND f\uZsxR@dD O  eME%hΦ@;̓@ ` ?\!4 4å&)| @ 0A!,?zp8@ 4Oɠ=00M%w  :IM;`h’&p40$z\X` GIƞT N`xuk=72ʘ_?, Pi  g(8)x8S>A0>@Bf@,SHz *=x>TCU>  d18 x  $bX0?1JRρOK 0,K ? zH4(4PU h@pʀ4|A0@CC\dh0A6,ԃGX40Q 4c/ " SM0@/ a?H Fpp@`~ 4jL!mO} 5X pL+40@Di?{0 h̅x7}<: (h!=M| (/ p z ߂D`@(_;W|\ ⯃D";+4?AD{GV &8zqU`V>`0pr=<<.Ã< d.x_^1  @cc"ߠcfj, 8/C} | ,|ɴa  t[I3i@0?g'[U ިH=<$;2 $j= i$ >)x^L%?5νhD0ʆ] ê.Wp3"Pt4_fMD;`_/>! A| A`7`c(}*pi>p2 m(4BY`ixx !FJUF/ Uh{X~CS+IEZ`x#VhC)N\<=# iǢY.t9|~CD 7p4`B0bރ4^ 4#' 4bPClHP ?R@ 7x(ހI 0NB]h”G\>p4_|| pq8<{C1{x0@9 6asz,xf~3ypl6SN[Tn6#:U " 'EuUcP}UJ<΅~B8tKX%JHX0> XY4O(j1,)~+3$gJp0Klx{Lt3OkuLQjSik,^ ͢Q5ɧK#4 h)u4E:%/%":T%)/5b4~p1k{xIU «Qvl}fŖbs~NJ*Z0C?A)0="4C$A_pW}h$ Q) p0*2V"X0@OʋBK pHՈꀠ4 07o4a]W hA6AU?k8qlA1':i@^Ix7 /р9upS DU"b%8_ 8|zgrZ& =Qz4;oz4˔leW `~ ,;=cm$򑏴Se;V%V( b[Ĺ3 oQA~3'p1^/xx_ hό/7|$@4dd<;h@~jX&Tz|VL| fcUN@ `ƹ3q|^4@C  %Ob6*F $ &8gۗ#lw`Q N&v5 *xGQ'+P<5GV`YXG i~K4W֏7SHjxӨ˛ɟG*sU#Oς4Z;߀Mpו=?= eQwPh4P7C#"y1yџW:mҥ68w?㒚Wp<:진{Sɉ8ͤu*Ե(DW:LgN*WcgmX4 @Ё! u)Ze9ƧHJ5z_\fМ<%8hu| &`4/|ɵu_׬Tx{kP^Tyg d)23)GR5z^C'ʹ"Z^zC h\`>T cNJy/?DžsǶ3ˆ==Qn 3T\\_@D>B>Aϟ \,|l\|ǺF`%@C1(Y83~[V|^^~xW/?xW},7(ϵSxc=7+=`Y `=hi,Dsa9)) 71yy[xk D 4,8 }l 'yvݬY_x,ަBc6xi4|i67 "h 1??h47ok31+hgs8xhxhGl3 L2  ?2 Kyˁ|xgD` '6Ex^q? 2R Xg೿d͈@' ml}~s`{5kջh_jh 7Jga›J]*$ ?ߏ^ 'Cj\]|p3Gd4`LIJW4 NIJvC+?h >?z 1uj|#X@&|26z`e.qW-1Տ|~pOJU$'MUL}P̓"Iңހ#rtk9Jt~ :i'zU_#xl{Z簜Ŵpl5׸g`W'. ZiC8]Q+ $n/Y^$$$%Is6] "?N9({A¥ÙNnf$浂g-/+,^," aUD 1<_ǣPhihxdeЁ'NV}ly&SMX4?G<ZKJ^1Q4:hx B! (p}$r,}ً܌3`A& %< XD!*lK3(Te 2`ߝp=#3L&{x4 ^ h?iDaш*$Y?@A `UE:I WAɇ[dO!tH)SyqUŬaj4L8sdyᗜ@74`BV<}_eh)jİh_O"|MLG]2Ebe>Mڷ t|q e 0àh('O*S>pʓ i xY%0&CphA~Eᘚ. P4υ4_b$sVm4CϺPg_ʫ@4p4B Sf^yFꭰ  sQD_LAV=H|4Bd_WB4/ހпZNώ€03 %rVe| >c7j@ @0 Vtzu!gV ޖ@L)uЁ jpB4<1T3S^`M4?W`@hc:@\> IOh*WM$x[2HB>&ؔVxɤz@пApܧʨ:GU4Oto{ >/5ZW3 U`)4`p(4>[zy~}LzzS:}'Gv (䦾JQ_TeIj:|3!%`|uf:?((4?"J9jO>1J~lԐPh_`fHӪ*a8|d2 -P4A N՜KRt^ y$v#{ m > "%'iH /fgM:h'8#8QSՃAu+JiOvh7Zu߯:תNjݪp 81ǩuicXqXa>%_#De0w@l4H2h?域GFZ, p1`78oA|]ϟ$tta0 xgzۿ2)2oWxh  L*_1 " h 1?m6x#83cxL40E}} @#C?_ Шaφn)ė4?dhUC|~hgh^lǛ/V_C*>){@<S@ /s v45 %3 hay|e%C/X0haxt [ 4k C(U㩫\%`4k'I^i]011>;2_xHj 9@ƈT)}VIJ/x/C a8 4W(=4 hir (UC/? /42 8^bP8 1Ͽ $#fr 'bqi?X4O^2&_58?~%Y%%%(2 u*V>ΐS|)31t4Cz#b<|f*J ϛ^ſ1 @Z 8 @ Նb:::9a\^2c. R,,x(4%T3=\tU OQ@$u^2+WADgdO d%A.P6QB%U`y\7Hg_O ldu"^ 8JĿ;#A\  edއeTW#$= 98]OOZGΆI.CM%N< 4)wH¿ HfGC(tBfxfnO C+:nmS \KW;/VQg.4B`2FE~k`]ZE#WƁ@jDtF8 $ ֢W/ZA4hB /NƏ~||f02WAxD?.5OBV2̜x h'-Y@4^`ĵ`@i`cw~3 p8¡,D!x AؐmY?A_T 0{~Fp1ZºՈz5+i`xЄ ߆# $@T1` 8H08$`G`40waJ k}/)y45N #Ph?p %b fS~ѤR  fY|x*~~Y0/ 9~1.g3df#U*A Ta2` ȖvvyA4.?5=ׅ4Gf"ACW0 @4v+VJ#@pt#' .X QPhe GV?~>4#7Wn"[!/p_1 - yP40gA#ư@$ HC0KhuT2U  h+4l h/#ĒaD?V 02,h AT3ZP j`hj$C"ޅO^%>E> ~ 7A`J\$``]\ e3 r/\ U`! l,K)W_Go@h;9PGD (fbh2Ж%P6r eӿHR0lJyT4v"`9eo{ !gUЭW?iAJ@`C F2JՈȆ:f q ŽV ;orQ(Al/`A/J 8dՈˀg#Y}@:` h b0?!S@;RbXx  ,``GU .(^ `|@^l_~ "O|lltksf@ /=ӚN_:C4tv|@п4I) >!`0/%~4{ǜx.=UNx€h@DlG9BLMB_4E>A& cLa1*$%۾0S@d *z p/JS Gx?h?@H/Y>%\+C(*u[8$Bz|9 V d %iaJ2A|_0:!H0&`D8L,W:/޵UᘯQ+ 0IY)0>`000*>TJU>tᘀB/X3p3 'gF*2Tpϣ\*ずm0C4ۡ yR@.Xf&ĬDyLD*va)84c! _İh!''_$fLjJ #zn@ {. qz 做 l'ԑ[Bywx[Fi)9۱ f]Q$\H޹;5msLu8/ȹS{- ԙR*d #3о5:xxoߕ)I룄W{b|||w+j%C$}Nkd nOdžfzB':fP ǃ3;A~<>DwC'MA8m3rgSHN92e9k hc!s cg.U`]VWǻ{{"€h4?pn/L3~ # zqP0h06::=owE0 k8@ ^[pg!!!"_y {8 #9 XCh?צּA::4w4 Gf mttlor? Q@DyQhG$ >6667f}0Ph& 0 M& 0 MQOkH lV`lH pV`ou )! "h 1?K=pZ0AW[7~?%C ?*B1(_0 *40j/{pcPof w P0p|hp  m%CBC@ h6>:Ci?wP4@?T; hhgþ(AC$Hf?f2 xdA|M 0 ~ 3FJJONx ח~`3co9;Ga?G\3BG7{/#q09?@_^U5`<3xrAh_UCwH|&<"ge(E(59 [x.-u5hx C&/,^+ J0 6չ!),{,%j>Ap&\Ӈ' A Kၠy_$˄Bc LA&?18'*OTDL3XxAǽa,D$&"7=:.>$0(S#L<:xS&)6hါqȵD 00T?Ёi/Q6`K2D<|g?(PP"Z.T'h@F%,3G~X0 TpӰh@<n I+A{dzIueǍ<*D[0^*p` ?}T 2 087 c \ [=` `|x0 ɏ`,Vo|[.6.5P\2h~KGcA6$%MadzVd(^/@3xFJmX^83Pi|K_P6D GpkGLzhJ2M4y $<\л÷& cž+Roa+/ZhŠ@ h3?Ox$e_~ w ![$@ 8/{0@=E=*~11 H0> ?e= /@?Wߧkdh;Y]XZ L@ 9MG/hDž9jxA|VJh-3Sj̼x )F< J]Uv 1}ۼ?i1Dž8!h`ć ""> -`z2 Qpa<\@, hX.I!G>G?B DK?"o 0|Tj @&&)Opߟɀ^p\_ngj ^8*@;d3"`#.23y[_!*vxiEœQ~_( C8xœ;՗x c<Jf<4_ӆ/4ZUү l+N{σA&0x`@n_0IS# @* p* G` pU5DB*8#VnJnAbz. ,@h0` : PB^4 <. h0`bQ̽4D`.`Er\$F 029 .  0 v  p\!m+Ξ # ’Zg~=,J8a>@K>R_q}  K g.|ᒰh0,B @R 2* h}WzO_| #CAJPI@hp8 SQ ~p4?=AzDTeG)p)0 65L/h@,`aKjJ ?AVT >  ,^  `}GQ: X]tTp@ )2!ƿ?C8,S J%@>Dꮻ3M.A.AXW몜I.ij$BGW\z0Q šA_!P`AnI#ġ+=~^~z(4OM ثJzq>CttPU\:  *Cp  B?iG<`8 Zv<~ĨiN^=WyM yz@HWi%ᘎaiL$ypc1y%%dT d3!Nzqs$pWc$d3š?@/\lH*Bo4VUc]`hB7ᰣ3x4P_2 4$Q@id1x?2pdW}7G *HS,Xf`oH>jB=T0:JTS ]uP40Ozx4 _< K?h@pa:aO|xf f˧"9>EPJ*f$Jdp}C!,hH40qo=A -[<ʃ@4Cdפ (ٚj99))7SU`~3+s5`6nd H{'۫}coLq 2ƙ~t?@,2zpƙ/Sj(|z ܔhKE#Rqʜ)*Y2LL}ש78@ hU,2G= {\\\_ q|S  @A|V{P2 a>c!B4?O k}w@>>@ qp3m'3~wN`hA\Pg >8JJBAޝ#C@Ph!A*.!/(FFur2,(4!b|LDC4!`fe@˃9HxxT? ߎ dhցXBB<x 0](d%8v 3Y (p4w@  z"h 1d~~sӧ'} +=Xe8`/,Ma28xx|4@Uwb`hq2Ah8xxx{?x@s`g * [ɓ6 wq11Q/vk(YG?<q݃L2 a~C"7_79L2uaZ&qqqqOᓝ\@eALosO[7w^2bPm]xM`~nx3/xۤnσCπHf7BB>=I  /;%˿? k@gh>V,BBBE•?6+ >2l 5_Cch0rsq=Ӥ2z^񳋩êYP* 箻N gD&6lMϺnuungXgs ʆL6j S{αȷ'i Y ]f}Ēz5d.> *Q9AUj1^hxgMUw{_`>B '/)Cϫgϫ Fx8jޭ3fڮ"M#пZ$U i'O8pQMP'xP`&\n`+7@ `@-2~C$&%pdC7 ϓ4[zKW4/V4^ ! D%>= gi|p4$1B5 5I:(!y9#u pW B4M]p4 Ch{9 As.J/3 0>Jߟ0$p0ė$p G$A V 0 7A+q0 =`|Ue<[0p/%t[x05I\>@P`V@ ʲ2``0! x"ƒ@ L =; XfH HO`8md >@gGᵟ<L`#d?|C(%]0?-@H `*V,!qxS`Q> *e03< =V$Bs1<7H^d\|P dݐ%`$눋% >MɠCnK7| 0fœ*6wUSh?BgXaSQaGF=U:@ /w!XP  xp2X Į0'mHgeqN82RR'̿? ?| 7%FC(yq 2~10"RRi*U0 a2G?CD돥u 5Kp t14O~X+/eYaϰhS@ h0h|dgɣLDw޳7\/4Nsh` 'N˟CoՀPeO{N%ASAd\\[{D1@GAԹ9G~<ǏMCп9EQ׼bC}ux42@{ t42}6QW>0 ]/wŠ]8=G$*X2t}}42N'W8F?:N $NOӰ[)V#_cjQhh_%C.2213 pȠA3{}?[8nےRg^C'i>=v%3#sa`>X+qȤ.8K; }rk֜ǜ.WWitph@ />,p Th S+tttwomk9{0ˀ[SjsN>{JWS GSorxfpdzp4z8|w\ \aqh8g! 3@E[@,^6665 $$$?w< qqUя`G? 3o~II?RVRU3~~^"A #" {v Z+$ddc?amC?4C-DWW.XBq>Dᾶowp놰>C†p+o qX^  "h 2?7xa =g3~/8>mo@3o78hhhgkB\30 G&&"%n" K7ko0 (rhd^RJޏ7հ p?u<{deZR8,-82st:z44Njoz*'}<4_c }Csk_ :fftUP99'x}Ϥ0Jxf0g@Ϲya_ ҳ0Y3[_8av<rҲE0rx 9iyYgὟO^x8mpמC(ꏶP oZ6Qgzzm|3>,m{ӁDspVs=W*7s#|m9 (GM$T,޿^~o* 4 bZxf%I$y}dSaҚRg`xo8{c 8ׂAdƪUP rO<`QCp=ã׾m˸TgĻϼ + -^# c &I)'(om@i=X] hB9ljDFEQQSڈ盐f%.Y9:Ö3P+a׆)WKKK??=GέI[ڛգϖ `e[2sT>'V g'+'(o.GG~'Tuپ/[-]ʍEGCТ>w"B3{KO k7"IWߡ OG9V爗ϽJ=5.8Gfp0|IyYgK?>l@\f&>t=N9 g1UG=L-?xyʁ. )sL B+Nw%{7^79SsM{˨gC  述~׵U ah>~sjNJ(%ia#sW{+@Z<^Nsti@п*<6mW->mOiv={٧d\\ ދs|>lllkg|^  (50III?W=~e,^Mcupi T,"h 2z pPPP1A: .(' Ն@.?np"""#F_CBBB_sЙ>딪q]ǹhav/VU؀+ xngGy,rvk>@L3 ᜄxeqѱTnNnG+1B3zfyJfN8Uп7 bGr:rs1#"sɊ{3wz82}5Lrdp/q'M^_貫 λ78T#2`z. G^3+$GUhW 奟5[h`πKR2S$u2:q|2]31Hy`pSP8!lllkVL+Ul3 # |z. Gp`b'{T7;9tI6}qg4rrzj2=O$ڨ  !JFS`vJfw^(f~?M}kN> a޼Uz204__j\^~Io=pd͉mހxG`00H_| o`8P`^~F2$'`AebڄayqFܪ y=P3^?:0G sO;Zfl5׀GlwO742jC=i1!118EF޻J4?ЯC\yPr< ~=OG.}8R.zD@A>>*d}L/LӮ:앧ub=@sSg)))(h4j9gl;uhv_Ph hYg1s{tqTږE_y7ipud27]/޻9{]wkiڽ+;5$iwW}ys[p~FFqѱVVZ[ᧆqQQQ/Xgᜄ"%+n9III?_q_ N" h 2i~@ а~`/do/xG`7Om տWs>vh2817~+* Yz|[o~V><=[r}uTzmrBx==ɸfy%ǔ}y)g 3YZn7Νܯ`4?˼&i0 >^0&s +>6֋ԳmIhĠ`aGdA2qv蝟.oM,9IIIG{ Y5!4pH(_{V#`†o 7P`^~F0% '`e0āxuy ' As 3`f{7tᕷէ2: ϙ7o^g{ YM< )f|U>{ûkr_$vvGG5Os3dgﭻ mo"$h 23~t8xxgtt|[w:>>?t}g7L謘ebOj5Y"""#:}{t2e]N_!& ""!߾[ ϓN7O:z}2{VznOefώ]{D}}83cn%w_ +2Zoa`Ё[ͺE_M/4 =>hg\5Ǩiz2nSS<}t㜫(e Z{!ޚ?W[uFui=~9oCh3lgt{mϫf3{A 2rhO_ ̻1 +G^qghV{ރ:*]yƿ|4O{upĄD̷Sq9g&o;$ Fϥ!_g ʢ1Є *( QBQLl! ^ CX| ii~DH<35h`4, 'CѼmg7&?)}3{޸n8V/e4* ԛ?+5 zM<7&iV gG iƭ|0x'MQy^P2hs[ ܢtdyߦ5ېu[ᓨ\ Ina`$͆zm!{{]~>o}YL:iiO/@{P{fӵgVe\8ڧ ެyeoP 8L+oAS e ҝmw #ߞ7WfE?L3cp3 ߿?$'H8hhh_nx "(h 2-w~(`8; ߔB{xp29A5بׁ6T g,HeQVIII?`bz||M˔d!OdqQQ1OjE_Vca_mP<H2wEU}~{:O?}`rlΏ{Ng: ʹUT{)g;)CBWVSr<^67yg9W+pاJqDGd tYTe ׯ<2i_O%E8fk$aKB 8ȸȷ26251n޿j:3$HZ#}#5L'/$wEw5#'C7.8 a yR p3{j~A^HT/&G7 Ue ǝ2+Hotk 3}~@I E` 1290]n@in 䤤$__d0r}y[odHB~tOU8q9&ޤZonzPrxXN< ?+a4L: DsOՁ¸Ɯꉆo ⢢8_l]᜔q[3~~rs_dg/;Ws9IIIG LUO .ǿ/x3ntW GGG?=G7'Z3?89 ?ή ",h 3?9_''agAB1 _n ^v\#@OYg wo?]3 ?"pA/ pЏ6:67b|Dg {C7(e{E&pەq]0VVVYb>i-!:xܓ%8 +?A@v鱼Y^FJFE ; `0>kYn{E #H:Motw^ܓ6L{ǝ?=C0.625f>PЄ #@OﯯrRRRO8N  >Vj@DPNN[;zP<;E05S> >$xn*`3~}|^JAW}0?'zR`( "bώ6}篓['}cR"x0~dᘳ}W3}~}ݹ<{sMU011!!8G?"[8sۣoM}oksc>|ƸRϋ;>O|UNzɹ}P'FFF sVY}?{~\.HnVY9gW޳b=s:Sݽ'o[~}㗉Vj-*p[~nn8k gk wb|DDD qp&&(2k~7OC7gĆQq|3o \x| E*0h  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbd`@B `0^ƒ iF0`9l P` 4+06 (0Br 0hVP`9k P` 4/A/`aA4Ьr 0hX2 X0^ƒ iY/`aA4а0_f`@B X0^ƒ i`  0 `{ g`@BB@Ѥ0`!M 0 `{ H0@5 (0@ ` M F`8k Pp 4+@0@5 (8@@q 0hf`8j Pp 44DG/`aA4@q 0hh5 P0^ƒi/`aA4/j`C5 P0^ƒi"`_ 0 @{ k`CDQ` M 0 @z ~h0 5 (8@` M F`8Aj Pp 43h0 5 (8@./p 0hf`8Aj Pp 44\\ A/PaA4͠p 0hh6 P0^ƒiA/PaA4qp/m`AC6 P0^ƒi_h 0 @z m`ACEѴ` M 0 @z }h0 5 (8@` M F`8Aj Pp 43h0 5 (8@..p 0hf`8Aj Pp 44\\ /`aA4̀p 0hh5 P0^ƒi/`aA41P/k`C5 P0^ƒibߨ 0 @{ j`CDQ ` M 0 @{ ~P0@5 (8@ ` M F`8k Pp 4+@0@5 (8@q 0hV`8k Pp 4,4/A/`aA4Эq 0hXho3 X0^ƒ iY/`aA4а/g`@B X0^ƒ ia! _ 0 `{ e`@BQ@`!M  0 `{ ~ 06 (0B@`!M F0`9l P` 4+06 (0B`r 0hV `9l P` 4+/paA4Ь r 0hW0 h0^ƒ iXA/paA4Яo  `8(/AB~p?󆅆~<>x |eA ppp󃄆,<4?"?8DDN&*./qQQq󊋋Td\o##cߧ8N::>?qӎ|tt|g8G:667qﳋ}\\\_bߧ8DN""#pӆ~4447aaߧ  8(H?AAN o0?~~ ePpPPp󃄅,4<7ᡡ? 8DD&&&'qQ1qӋ~\\\o8N66>?qӏ~||$?'!!8 HN>>BCqӎ~ll|ccߧ8FN*&./qQ1q_Ӊ~DDLO"""?ߧ 8hhhoCCNpPӂ~ ߫ BBP󃃃,4$7ᡢ?8D&*./q11q󋋋~\\\occߧ8N>>>Cqӏ~$$$g!!%%9 HHHNBBBCrӏ~||$?g8FFN6.27qqqӋ~\T\Wbbߧ8CDDNpАӄ~$$``XP2$$7a8&&.+qqqq󋋌\\dg#c#ߧ8N>>>?qӐ~$$dg#%#%9)))/HBBFGr2?Ӑ~$$dߧ!!!!8N>6>;qﳍ~\\dgߧ8DEEN""&#pӆ~4$4/ a?ߧ8B>2ᡡ 8xxxDD&.*+qqqq󋌌\\dgccc8N>>>?qӐ~$$dߧ%%##9)))/IIIJJJKrRRR_ﳒ}$$dߧ%%##9 HHH>>>Cq}|loc#cߧ8N.../qQQ_ӈ~D4D7aߧ  8HHX?xC>!~,4,3!8.2.3qqq󍍍tlto8HHHNBFBGrRRR_Ӓ~䤤ߧ%%%%9)))/IIINJJJKrRRR_}䤤ߧ%%%%9 HHHNB>>?qӏ~|l|oc#c?ߧ8EN&&&'q11ӈ4447aߧ  8:ЯӉ~DDDCbb"8E6667qѱ󏏏|||ߧ!!9 ) /IIINJJJKrRRR_Ӓ~䤤ߧ%%%%9)))/IIIJJJKrRRR_ﳒ~䤤ߧ%%%%9 HHHNB>B?qӏ~|t|wcccߧ8DDN&"&'q11󈆇4,4/!!!?ߧXBĺp󈈈DLLSb8>>>CqӐ~$$$!#!$9)))/IIINJJJIrRRROӒ~%%%ߧ))))999IOJNRJRKrRRR_Ӓ}䤤ߧ#%#%9 HHHNB>BCqӏ~lllo##cߧ8DDN&"&#qӆ4447!!!?߫xC""&!q111?󊋊\\dc#"8G>BBCrӐ~~dߧ%%%%9)))/JI?NJJJIrRrӔ~%%%?ߧ))))9IIIOIINNNRSrrrӒ~䤤ߧ##%%9)/IHNB>B?qӎ~tt|#ccߧ8EN""'pӆ~$$$'!b_8Eſ.2.1q󍍏|t|{d_!!! 9 )'I?NJJJKrRRROӒ~e%%ߧ'')(9IYIGJJ?NVVV[rӔ~%%%?ߧ++))9YIIOIJJRSrRRROﳒ~$g!%!8 N:>>?qѱӌ~Tdd_b_ߧ8xBBq1/󉉉~T\T[?8GG>>>Cr22󐐑~~䤤ߧ#%%'9))I'JIJ??NRRRQrӔ~~%%%?ߧ+-+)9YiYoKKJNZRRSrӔ~䤥%?ߧ')))9))IGIJI??NJJJIrR2R/Ӓ~$$?ߧ8FN2.2/qq1q_Ӊ~DBBAr󑐐~)=rx0@ .VP+mA|Q*h AoFV_䓒|$ߧ)))(9IIIGJJJ?NRRV[rӕ~eeߧ))-,9IYigJK?NZZVYrӔ~$%ߧ%%)(^JL쓠0 PcϓH(?T ʬ <A2~JJ(IQ|$$$8FƿN2.2/qqqqӋL>>9qѱӌ~l\l[bߧ8xwCCC?VȘ`h"_8F:1я~N6`0CBR ^.`iAV$dx0 GHD8ArE ,8*yh0s7pӰ$JK!`:|H?Iʼn>P_a,yT*A0B1@W@ !0~`?6`K x]ف0 RC|bX \?Aq\m"rVIZ]q󖖖Jo@ /y ʷCH{W=D+WΌ+y\@@IYi2t*WsBX~a hġ#|%4^h$+.*$#6ӑ @?s$^?0֬Bǔ`*TV%*AQRv90`gOOf~qRr9  ~~; yyy`  T\\_0+ P DЌXFso h00" 90r?󑏑~@ lHP@\\6$ /뉍0`aդxCY`4i_' +! 1`a|D 0?>#.P/bbg?ߟA3C_U4Ahy3⇐| )v'@8 f bWm`AICD r -/-/#P0` }0 ‹  /YWJS2V@`cH / D`O'1)09& `>1t"` %zs8^$6`; H__/xlS"/~.>Q/ zqm=o x?DʫM>V\v:$QL5{jh(ӄD#8J ܴ Hgh0 Ćq*̿?,li,?!p`Ϳ1LN z (|C/= >!D#.q! Bc 2B AVG!9  x0>D`aD`aQ0(`| /K Z֒ H- }Ar-Z09['rĢ`0EpvIbW IT`1W?RE0`B&@`ЄT^gl h!~0 ‰ + ee---/U0h#N ] ! 5"wT[-/*ǾMnjIG`O8(0@OV| l6`> WbP `kSZ$(OρQU_S^= Oe7H/" G>hh6(U:\WJ=,|^+L'A De (bj/07f8z`}C"p22/<80HftҢge4HUf4(o΁lllkx ` f梆D̉A; |A:0 @isDDDGa."3~d\\[#c#_f:2 1qD 1 ǀ<o x00ȿk +#dr?󑑒|\0*#TE p0>D`aUA=bCH003āA/a4dr Ҳ9X 0?># '`Ȕ (eq2@`0 `~Ex0??#ZR}~;0P|4"|T_%pp40OϸH0 @8 p$$>4! U`MhaE`ʰF 0`=8 0 R!$P`A wYbyybyWȪ 0ŠB ( gV p`0 RKK>?a5jh0K|LBDE 'N""pa8gĿ:qqqQo󍎍~tdt{xL\|#?Z*RGx00ȄIw 0`|I( _䤤d_0`Д /9IE& ,6@ _`~`? TUDQd/b`9bo/g|Y0 exIy]UApt+:+sqB`,H1 /RbX0?U(VPe-B (0^:ӿ.* 40 4@|HeeeA}ǟ߷--MKGKfi|8 p0EקFv @( xJ`İd0 > (lQ`}oa KKK>Tۥd˛sd`ba0 8R f$OA f8JT0`J'(O)M>`aGD  P/A /Ƞ h0?!0 9#o󒑓Bim^@Q8hHQP40BPO5F%5*)OJ}F~7 MA*?J($&'8~ Ra"Vc/=cq󆆆11?sdtdkq~aw+5`I:E`A U{bcS5#S! +.ӂُ&/._UhG'Ċ4$GҬ` ~&(b 4&=F%jxXT/f`?TJVÿN'K5ƅ ^9PH@8B.pO)u קasAB?1^=zJ*q 爵{ӛ/T_ ~7'@(bBL  Bx/% Ȳ0¸Mǀ X%Haڥx #--1,^ZLG-%2~|@  . G ̽k` 0`~x A/#{teX|_O!p 2VR.Lt!ʫߌBW5SOA?v`:cI`U@v £$&A.' Qhd` ǁ 2V\|C94O5DJr*(LsT%*Ђ0 ʼ#=AoV$DW`q㗕tB~B ' 2 .A.O,YP`~C2 P  }{1-1.nZKl2tn~|. G<< Fï4Dh0?*ρmxJ/ hi;LP2tA`!)&~\8i`]e?HDw Wp?:<z0%e7ث޲?  8Uǿa_5堀 -T`Ara/iUGwDD Ue~ؠȾ ?x0 >a `X@`>?>.?Kbb8hhho"bb..21q󎎎~||# /d$_ HX0 qH)XNK_"999)7JJJ?NRRM9I9bRU4K.j%E)*G@`:2`7 jWʿ_|奥e---,9iiioJ|C~x3-/G%_ ^ 'i_IU/=-19iyyo˿bL_ˮ50qBX0E=KKʽrJZUc?Q+,D^^ߧ+))*9II9/I.eQP0 o~dߨ/bKğNFBBCqѱѿ֊pp0cFcQ??/~ڰAE|?n6+0;8xCCCV5QO"8Gǿ>B>Ar2/󑑐~䤤')'*9IIYWJJJ??NRVRQrҲ󖖖~奥----9iiigKKKNZ^Z_rӗ~g///.9iiioKKKNZZZWrӔ~䤤g%#%#9 NB>>?qѱѿӌ~\L\Kbb8CBV<Hh"8F>B>Ar󐑐~䤤%%%$9II9GJJJ??NRRRQrҒӖ~~奥---,9iiigN^^^_rӗ~ߧ////9yyyKKK??NZZZYrӖ~$$%%%$9))HHHN>>>?qӌ~\\\_bߧ8xCCCdhe8FF6:6;q󐐐~䤤%%%$9IIIGJJJ?RRRQrӔ~~奥---,9iiigKKK?NZZZ[rӖ~ߧ////9yyyN^^^]rӕ~$$ߧ%%%$9))HHGN>>>?qӎ~d\d_ߧ8CCN""pt\[8ƿ2:25q󐐐~$d$%%%$9))9'III?NRNQr󔕔eߧ---,9iiigKKKNZZZ[rӖ~ߧ---.9yyywKKK?NZZZ[rӕ~$ߧ%%%%9))HHHN>>>?qӌ~\\\_bߧ8CCNp󊉈~ddL[#"_8HH?BBBAr/󒒒~䤤%'%$999)OJJJRVRSrӕ~奥ߧ++/-9YiYoKKKNZZZ[rӖ~奥ߧ----9iYIOJJNRNRKrrRR_Ӓ~##ߧ8EEN.**/qQ1Q?Ӊ~DDDO!ߧ 8HHHOD**.1qqq󌍌ltlsc!!! 9  IHH?FJJIrRRRO󒓒%$ߧ))%(9IIIGJJJNVVRSrӖ~eeߧ+---9iiioKKKNZZZ[rӕ~%$ߧ'%%%9)))HHHNBBB?rӏ~ll\_""ߧ8DDDN"""qпӅ~$$0Cb8FF:>:?r󐐏~䤤d%%%%9)))/IIINJNJKrRRӔ~%%%?ߧ))))9YiIOKJJNZZR[rҲҟӖ~奥%?ߧ-+))9IIIOJINNJJKrRR2?Ӓ~#ߧ8FN..*+qQ11Ӊ~DDD?!᡿ߧ 8HXHOAAAq󊉊T\TW#"8G>BBAr󐐐䤤d%%#%9)))/JIIRRJOrrӔ~%%%?ߧ))')9IiIOJJJNRRRSrӔ~%$ߧ'%%%9))HHNBBB?rӏ~||loc#cߧ8DDDN"""qӆ~4$,'!!!?X0 ?8E.6*3qѱ󏏎|||!!!9 HBJBKrRRR_󒒒~䤤ߧ%%%%9II9?JINRRJOrrӔ~%$ߧ))''9II9?IIINJJFGr22ﳐ}|ߧ8EN&&&'q11ӈ~<4<7ᡡߧ 8H88?BAAPgt`y}4< X0Q@¼i 0 `E j|`y}&% ` M 0 @E LK`05 +@ ` M`'j ,W 7h05 +@1qO X0!o`'j ,W 7cc(a^B4MO X0!o7@ H0Q`¼i(a^B4_n`y}7@ H0Q`¼i 0 E o`y}BA `A M 0 E ̌0 4 +@ `̄04 +@ `̄04 +@ `̄04 +@ ` M)' `(Ah ,W 7Ӑ0 4 +@2RN@P X0!o `(Ah ,W 7d(a^B4N@P X0!oI9B @0Q`¼i(a^B4_ْr`y}9B @0Q`¼i߳%$ 0 E r`y}fJQ ` M 0 E ~̤0 4 +@ ` M)' `(Ah ,W 7Ӑ0 4 +@2RN@P X0!o `(Ah ,W 7d(a^B4N@P X0!oI9B @0Q`¼i(a^B4_ْr`y}9B @0Q`¼i߳'$ 0 E q`y}fBA `A M 0 E ~̌{04 +@ `A M`(i ,W 7Ӏ04 +@1P X0!o`(i ,W 7cc(a^B4MO X0!o7> P0Q`¼iA(a^B4_ًm|`y}6> P0Q`¼i߳ 0 @E k|`y}f"! ` M 0 @E ~<3P05 +@ ` Mhf`'j (W 7H05 +@0Ѝ O P0!op`'Ak (W 7a (a^4lN P0AoA3: X0Q@½ iA(a^4ep`z@}8 `0Q@½ i 0 E dp`z@}L@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb?E5 "4h 37 ? ⢢_y:EI3~3!!yxq11?}X3~'w63ۻ{ =;Vӂ+Ɖɏ?u>=p7tf&(&%ܜ(ٷ.6)&Оom[y~zm?s%zmmYOG74"PhߑR/&~|w7+mɾ'xy?^u$LHLJG똄9XξxwDDZ<޳o?: ɹu'v&ɏ,q4?W Dbe|q5=5G'?m g>uuy!7y+/ C" 4?obC!H%q Bp0 A-1<30-g---07,%. }w">업<gIʿ`j; v9=1 ZE@h`^RN4O8`A8 0g7  SY. G=HDqWz2 ?M*޴;!8釦A>~9 q@ ŹRsӀf$$"!0d}|osɞlYݶ40k2sAD=|~| C& 40boAn( @4?Bm@4C3yS f6?O`~7BC0ι,1qӜx{vڳ‰9Pdc6:{:sn+,mrNsm>GG9&{mqu3' g/ "_2DEC>-fEDDq111Og)8 {"8h 3wG4{"ĺ%""3pS{ū y3~~+z|B1[|^}gny4Οe'W[Tzo~-qw{}GݵNz$<]cLL[S瞦GYzx[r>9YyYw$Ypf"&"% w}݈}&$xm^tnwwzw\Xlz^ODiW{pJ};}&y ){x}2$tt\nm?{kַ {ZTs{Eu0oG?=ZݶWK=']ή~,rgӮWUwҹw_i뻤{w>ɿ~y 'x 7"/ a{<}~8\ޯg> xc?ɱW[y-{*{зU'|zoI<}y8|-Qx>2k &w8^~HnO'yJ|w73]=+|{0 ֜svqŒַ[ܴOWvCyt|WGcoۣSد3~}#owE&ଞ8NiqN&ws6|Mޡo>۾=[UɝnNC}^vvؑ|0GmPzՈdds5Ξz[z}J{{MJptm-k{y5'cy= >{G_ y^D)6+GW+@ۚj>?{*yﴯC!Y\nortÞ}+G~,dاx1C*&'wWm3}<y |߾m-'b.(ľ?0r|EL͚>菹iӳ]kaP'cxM+߮ ].̒P;Eg^Rn{޿^4^6ft@&P`{ULJcx U_LWJiz !+Aup4{ {}}1 sz*9;hab  ,Cen  //_qB[r;?a;VWrYG+û'OQ:>0mGqLgۺDF̟i ɹCVm6Z{.{F ѫ&xݧ:kAV䤵&{Oq˶ra{iw[Y attl{c~Y"uq݄^nmcGZiFa`X}FO{C5ﻉ:9|7'q^B[~z>oZIՍpIȓw9nӟkKK8{Ah_$[YKhܻ}=ޚ*ƿixg/rw1B$<a?1'Vn_3j՛C8ȸi?=]C#+;w_JJNI^3 ]gfiϟg #c#C8:{NcP00((@3 奥9{ Q1LHbWuq{Ye'6};q۴yNposrr4wa;G/6OY EFJ$>ݝId'KIޞttNNsN3:}3Y\ҽңrcldlcߕat2}>M>TkʍߤLLn5'j{=ܮ3w>9@hc!Hg-mOzT[9iiiW{PyPdK#3>g9W}g>'_e_LeUnbbbpNax0L > 0`a8=Phׁ<haa``Ag #b gLmN:/Jq$29Jy㎆^Xf\?zU{8BC3B'] !,yŹ0;K6z*2~+=uxgW<}sO}:ܲgsͫS뫃%pl Pg 3/*́i 81o` ߝfx29o ǩ;%|띡SO<'ts_k {2Ѥ}GNHHHg!!! }d5Tj"*F ?"Hh 3?{>_.}[g'S.Y^} !!`ƙ^o)hhG\?x"#۷&x3-f%l˭{Pqe83~ڑa=K[iS޴y=Ry;NɄNyVkr{O>܏TNs?[~,_IϿ Yϼdd'l=鄌V[ɞ-Bϊ 5Dqzk[uU=͟v~cs{zLǹ3t*{Ϸys+/=w>Jxb5dzQ;yI ɬKHZ'=n -)IDy'GWI]B}?sŊϷ8se'׵~ԏ>yߡHos{d羵b[<;/{!Ifu]o<y}rܯ\Yޟlkwo}~Hȿ7?wd㺻o ᡡ^ N[umnou2mGJ "Lh 3?  ^:;2jZ+{C8xx^^FFFYC,^g  "5  2TjpE5F@>.4V4(FgG6w>~Jcj&7Ƣߩ{ $$_s}yC9IIIWxB DӡfﱹZVZYN緆M,?{KǸ]Cׯ|Yﰕ3 dddu{ip,}7 5>}a'c;Eo׹O/l3~ dž"g3 d__S4/vO>W ?cZ{< /x >}ޢo*+߆qѱѯ,Ls_ΝCω[>b.Y3H}o'k>{>vOFUibcjdUS뷺{ Ą/ɖLEO-Ndū6ndOڦp:>:9eMu`MO<~shn7Bqwyuru#y撯S~ؽdgCsg#sg?Gwkpg{Cwa ඿0g㌲{}VK t>KK?HnsaE !_tkoySTO#qkButk/{i܎9_OCͣ8шY4$aɺCtM͔D 㣣ۇ\)tHH)obbbb*&}4t'v ,|hmGmWhU\}A>T/."tS+G{O|nGeq2G -3</gdZ0ǘc<{焽|{7@= RRRMi,3 =`hdzx4 {3TP`HC㠝 Ga7C8$2clY2Kv_J14Koy1+G Ur P?3x}B }DyS~qcv4Ϟ8͜ݍmP<nn a7NVNMյ{Пt]7O±Ǵ3~ 㣣c߹&E9t{f-7;+Y]uwVqusvۀ86665 f6625ltls|p/yl#_ӿ Z!"Xh 3_}y\wמ,w/a^g <:=}5+Q qzy4O3~qiazdC 8B2%0ɰ㫈~GXnyG`19IYIGoBw}1CAxY~8ȸ"'zgԚt()DB9X#rM6}΍__d0yrү!q+u>Ճxf&&&'4yV 8P&g;@D :!2@` ½ M\)}rp۽Z6n^'9[{Ŋ<2 5<.̜GS΢dΤSΟj  Ig@ 0ރ *(5Vup`|w@E3 udBd :/Oj#'=r`"Æqppg|Wg᜜LLLLg HDt|t{`d["XgSqqqg *../HHH \ e 4B"\h 4<&@,~{-xok{fo{[ {[[[{3 ަ7Mp{/po `.   P2ioxh  (p4kyx0_`M4n3~} ࠟC8(((H 3{ aaa`^8XXhhG 3| !aDAx7~*3z b"!8G""**3z "b"ރ8G&& 3z bb8 3{ "!8xxG3{ aaaa8XHXXG 3{ 88888  3{ ߠ_7' 3{{||{ ^^}y{{73} `C8OH' 3| a`8XX88'" 3| ᡢ!8xx'*&*2 s bb8'**.2 3{ bރ8G*&*. 3z bb8ȸ'&".* 3{ b" " 8hXh' 3{ a`8H8XX' 3{ ``8' 3}{ __@^ @ y7  3} a" C8HH(8' 3| !"!8xxXX'""* 3| "b 8'62&.3{ c?ރ8'2226 3{ ⢣# ރ8'2*66 3z 8'**22 3{yqQQ0oaDDDDgCDDA=pOa,,44g A=ppppOa g@=oOaf7p000_ g DBBqO4<g DC>qQpOa,,LLgEA=qQQpOaDD\TgqATTddgFF=qOAdlllgA=qQQOaddTTgEFFA=qQQpOATLd\gDDA=q0OaD<\Lg CCCA=pOa$$$g A=pP0P0Oaf=?A=pPP0?g BBpАOqqq0OaTTLTe?EEEA=qQqQOAdTddgEEA=qOallttgG=qQOAllttg=qOaTT\\g<qO!d\ddgDE=q11POcpOa44<<g CB=ppOa$g@=oOab~| fABp,$,7g CCq1Q0/\TTT gEE@qqOAdTd\g=qOallllgGA=qOwe@qCԸ  ygF=qѱOA|||gA<qOllllgFFFA=qQpOaT\TTgDD=qOaDDDDg  A=pOag=oOa _' 3| _C8HH""#3} aaC8***+3~ ⢢_8ظ'6666 3z cc8'>>>> Xw>M!Q zBA3t$@ yPKЁ@l|!GG3y c`C8>>>?3z 㣣ރ8'2222 3{ ###?8ȸȸ**** 3{ """?83{ 8((((' 3{ ߿ߠ8 3} _C8xhxo#3} bC8xxx***+3~ 8ظ'6>6> 3z cc8'>>>> װ G">xTTV:dQQQXa$D')@uOX/7oN c`G 8>>>?3z c`8'66663{ ""^8'**** 3{ b"b?8xXx_3{ `8' 3~~{@ pO$$b|T3L>V}S2#'0j8J7 =8XS$$,#g  EC(1_LLDGgEFqQQQ_dt\kgFGqѱOa||||gGGq1{B룴pqυo  3}>HG<r220OAg####HHqO||||gFGFA<qё/!tl|ogFqqQ/aLLLLgDDA=qOa4,,4g AA=ppPppOa d|'  3| p# _C8hx*3} cc _C822*)3| #c8:66;3| 㣣c`_8>BFF3x $"HqRP/A g!#%%I@r22R0/Ag####HIIAp0?Og ('" 3} "a_C8xh&3} #"c?C8**./3{ cރ86:653~ cd8FFFGsg####IIIApАO$$$$g >(wABpp/<4DT g EDDqQdTlSgFqqqddlogqal|{g%#! HFF>>g))#$dddc#I))Ǟ9))('JJJK3z 䤤^99)98BBNJr2R/n||| @BF9C@ w z:`u sNy9q| @$q8'66.. 3{ bb`^8'&"& 3{ ! 8HXHH' 3} `_"1D>.#3} ⢿C8xȨ66.33} Qq?C866:93} 㣿C8>>F?3z c=_z@J=ȸe_*{>rhArrqR ~"IpH ropW@ d9)))'JJJJ 3y 99)9/!d$$oF0T +h*<`h\ 2.rBS(:^1%JKK_t ii"x iJJˣґhe/0(JV%[6262 3z cbރ8&*&"3z 8xHxH' 3} ࠠhgĀT C83} bc_C8ب*&2)3} 㣣C86>:93} cC8(J>B9q0/GB@lT 3/`mJp@1Txd<釁!=irt@%%ӲM.4IIIA=rRRROAg%%%%@ '''{>>T}VBɎ U0/Is04*kh4BP^>Ț@jq)P4i>R@v~y:hhx2V,ؿ d=qOA\TTWgCpOA<<<<g  BBBA=pppOk (?pLTDSgq1|dtggFFqtllogqG|lLp"hϑQz8^.)A iTfiɬ#2#(KX` bKGܓo 䤤ރ9)))/JJJK3y,>7>>.<%@:/OyUٍJQ~<G=8=7yO? f4V?~ 83}w ccc`8'.*.* 3z aa8xxxx' 3z ^8(((' 08hgp<<  4{8^}1m?!G4$&9 5!zTNJJIYrRRROAg%%%%9)))w"YPe&$T '=95_xh`TVj40  $d$^B>B>3z c`x3y ccc`8'**** 3z ⢢8xx' 3z !a!`^8XHXH 3 a8&"&3} !bC8x22*33} #cC8>FBK3} 㤣@iG~>(OHG<G<Ӹ4I-H+xf6 "O~.=Yx4?pd8))'׽rRRR_Ag%%%%d$03hA~.6%DǧCSpP1I3 !~B>>>3z ^C8'6666 3z ⢠8'**** 3z aa8xxxx' 3z ``C87$gBqPdT\kgEqqLTdggGqѿ|tg!!!%q1?e'6b^*h`[91ESuhU @@-PRJFI!g%%%%III9(IHbUH2Ph_'i5:4>j<v@ N*_p@ C(BJ1EtIAt|lt g=qOATTTTgEEEA=q10OAD<<<gCA=p/A$gO4DLT g Cÿq1LLl_gFqQQѿltTWgGGFq1l|g!IG܌0B "t x)x$(9Pe/U ejd)@` /S|T#8IV dރ9)))(JJJK3z $^B| m0E==BPdWiɪ 4`A'aչ8]V>sx4{%T~:XG(=άX_H6:653| cca8'6266 3z ⢢⠞8'&& 3z 8xHxH'3| 0 q0D<F>=3}u%9?3POPP^ɠ7Od4⋸Czݧ$>)3RO g%%%%III@rRRRPO! g%%%%9)q\?^T?w- AA|%g#C (&(/ip(((+O ~mEK3qqq_{gA=qqpOA\TTTgC=pOA<,<,g BBA=pppPPOa igHwXxxLdT_g"!X|llggFqtltwgHq1|t;'(4=HXf8S_]:P@ JJJI3z 䤤d9)))'!IIHA= 䤤^9)))JFFA3zzbhIÏીS!(7* 0('d_ĐhA ؀.,@ollldgEEEA=qQQQPOA<<<<gA=p𰰐OA$$$$g  @@=Oa+ 8!3} bC8_2*"33} "?C8**./3} ߃8:J6C3} b](ғas@,j )sneF8A|4`IIH=r/AHg;Tׁ\T].I fheHHqAgSTj%QO`3ONPO[6[r% 3cVNlllkf>qQQQPOATTTTgDA=pOA4$4$g BBBA=pppOA  g4o bbC8xw&6&33} ""_C862633} C82>2?3~ c߃87y@ ME.R_Tj\3TRSSZVhAwf2 d$d JJ*xhd?  mG| :.hXh(d}N=0|5[!&2?"UìjFg####\9=FFBB 2 ?uKƙ{FXO> jϮi8 aK1uVq/ad\d\ gEEEq1110OaLLLLgBCBA=pOA$$g@@=oOA;`Cq111OLDL?gGE@q1Q10/T\T_gEEE?qtltogqQ_|6%C^6\iЀ5F5%=@L )`|dT@ JgDH0B|cG=zi_ h4AUUSƕXO(<Օ\0 4%Jlꏮ@dVRsg###"R'dDA| +p4ʞPz )y~ $ bq@8q82.2.3{ ⢢8' 3z !!! 8H8H8' 3{ ``7'3t3g C@q1Q1/TT\GgFEqQQ||logF?q/|l|ggGռjڬ4CX )>h *JnQ̼pyb{~/l?*/8gS/߀p Vka̸*.5j|UEK2 ψ\]9H ߼>{7V8qj=~65 Fƿy ccc`^8ȸب'*.** 3z ⢢`8xxx' 3z a! 8H8H8' 3{ ࠞ?' Џ !8x"*"3} bbC82*633} "C86>213~ 㣤c8>WDZ(f*O,3<*jkVi4K z4Q}@(|{8>B>B3~_3z cccރ8بب'**** 3z ba8xxxx' 3z ! 8HHHX' 3{ `8' |?g Cq??6>>>3{ $$$?4ǿ|<}[%E_2x(8G6::> 3y c#?ރ8Ȩ2*** 3z bb8'"""" 3z a! 8hHH('  3{ `7' 1¾3} ᡠ_C8xh"'3} b_C8ȷ22"!3} #b⠟C8'22:6 3| cc`8':::: 3{ c`8'>B:: 3{ ##8'>B>: 3} #㣠8':>66 3z ###`8Ȩب'**** 3{ bb`8x'"" 3{ a!a 8H(H('   3{ 7' 3} C8hhXo3} `!C8h6.&'3} cbC8'6>.. 3| cc8ظ'6666 3{ dcc`8'>>66 3{ #9'>>>: 3z 㣣C8'6666 3z c#c 8ȸ'2*2* 3z bb`8xx' 3{ ᡠ8XHX(' 3z `?' 3~~{ ࡠ C8XHHH' 3} ᡡC8xh'.&&" 3} c"⠟C8'26.2 3} `C8'*22. 3| ### 8'6666 3~ cc`8':>:: 3z cc`8'2222 3z cc# 8'2.2* 3{ bb 8x' 3z ᡡ8hhHH'  3z ࠠ`8'' 3| ߟ{7#80 00iY`}MB3 C >  W4߰O$;@0 00iZ`}M´  C > A W4߰O,3@0 00iZ`|M5  C > A W4߰OTKX0 00iZ`}Mô" C > A W4߰OTSX0 00i[`}ME6" C > A W4߰O\k`0 00i[`|ME6" C > A W4߰OTkX0 00i[A`|MŶ$ C > A W4߰OT[`0 00i[`|MF7" C > A W4߰Odcp0 00i[`|MƷ" C > A W4߰Olkp0 00i[`|MF7" C > A W4߰Odsp0 00i[(`a_~>1ѭH 00i[`|MƷ" C > A W4߰Oalcp0 00i[`|MF7" C > A W4߰OAdcp0 00i[`|MƷ" C > A W4߰OaTS`0 00iZ`|ME5" C > A W4߰OaTSX0 00i[`|MO 7<3{30xfaL3{ޞ{`A`A}.  Wi×@`#4(Pa_Kc@Ao'  WiX`A}. 9~~c<Ao0`#4(Pa_K|4 ;Dd`h & !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb`A| 40 0ci^ x  ( h6`;/aAAA?QA PA4l`w0_@‚}}0 0i؁`MF `;/aAA } > w0_@‚cA| 4`M p0`} >  w 0i`M F@`;/aAA } > w 0iف/aAAQ`| 4009 ( h888'4 0_@‚i0`M F`;s PA4lw 0ia ( p0`} > `| 4$,A/aAA 0_@‚}P09 ( hF`;As PA4qOth0`AM h0`| %MF`;As PA(h6`;As PA(h8hx74 0_ ‚j0`A{P09 ( 4@09 ( 4DDA/aAiځ/aAi" h0`| %M h0`| %M F`;As PA(h6`;As PA(h8h75 0_ ‚ @A 0_ ‚ @ACA=Ѥ`J `J v 0P4m v 0P4poth0`M p0`} > w 0iA/aABCAѤ`| 4#H09 ( hHhh74 0_@‚i0`M F`;As PA4lw 0i` p0`} > `| 4$,/aAA 0_@‚{809 ( h6p`;s PA4potf0`M p0`} > w 0iف/aAA=є`| 4(09 ( h8'2 0_@‚e0`MF@`;s PA4l`w 0i p0`} > A PA4oOtbA| 40 0iߟ`( x  ( h6`;/aAA>A>QA PA4lw0_@‚|{|0 0i`. x0_`M   (p4oOPOx{f7'ٿ@> p/PPo|  aaa` 8HHXH7BANpаo|A= G>A> P0/}a?`g 8(XHBBA>N pO{N q1QPOszDDLD"" 8ȨE...2qqqQPOs{TTd\⢣# ާ8'=N..*. q11Os{TTLT"b8'CDA=" pOs{,,44 a` 888'AA=  oOs~}~{^C?@ o?}$$, !`c|"!ag8hh'CCCA=** q1qqpOs{DT\T⢠^8'EFFA=N2666 qOszd\\\ 8'FEE=2266 qQQpOSzTLd\""`8x'CDC= pOs{$$$ 8(('= oOusd8((8ACN pppﳆ}<,L<!ag  8'DE=" q1QPO{dTdd⢢ 8'A=N6666 qOSz\\ddcc`g8'EFA=N6626 qqqOSzd\dd"8'DEEA= qOs{,$4, a  888('@= oOr?>pP/~4'aa?g  8HhH(DE@N&".*p/|LTTT c# '8ظ'=6266 qqOs{||t cc`^_=JW}P.DW1Ǟ8'FA>> qOSzllll"c g8'EFE=*.** qqQqPOs{DDDDᡡ  8XXXH'ABAA= ppPP0Os{_.PO}  0/}>>*C a(Hn)>2zRh%O9!4yOu#|]_ US䏏|`g8N6667qxdddd### ^g8ȨȨEEEA=*&*& q/s{<4<4a!a 8(((('= Ous _g 8XHhhBB@qQ_ﳉ}<4D/ag8EEN*>.> qO|\t\tuJ| D)w3MC' [ؑAg8'N>>>?qxl|l|c`'8ȸEFE@&&&& qQQQPOs{LDLGa` 8hHhX'AB Os~~{A>\,ࠠg 8hDqqpﳊ}qOszttll#$ 2ЂR fȊ\?Rf}̩%2_|s~A!,~Bng\!G~>J9󏑎x||ާ8'"" ppPﳃ}DD4[Q'g  8F&&./qQQ1_}ldto##b_'8'=N>>>>蜄|y&]ty$'>&OU/D?j_BB:=t$#`!###9 'HHNBBFCqOxtltl㣣c'8ؿ.*.* q1Q10os{LLLD"""   8hXXH'AB= pp0ppOs{ dO'ءOC@>"p/}T\>>>920/z $d$g##%!9 >qOSz\\\\8'A=""" pOSz4$4$ࠠ'op g 8hHCB@&2+qQ1ﳇ}dtLd c _g8بFN6.F;qqs| $#_'##9 9'Y |>%%#~<9))9('IIINJJJJrR2RP/Sy 9))(IA UB7hsT~[T>$0_A Ϳ& ph! Gyqqq/Szdddd bb`^8x'C= pаАOS|  ﳃ}DL,G!bg  8xG&&.)qq1ﳑ|d\tsc#?g!8zt$$ c"|CmꁠWOQ3h\ J$@ %A|{A.PzASJ"rRRRPOSz䤤^g'#'%9 (Y))/%׿ Ǟ<Q*- "9>z911Ζ0RRbKKMLalyUX=$J=J=%E|qxeSzTTdd^8C=N pOSz$$$$ࠠ* L; AXCB?&*+qPQoﳇ}\tL[cg8H?>6>;qﳐ}|| _IDU{Yp4f*kED }%oP f:ht 毡B% $h_{TjrRRP/Sz䤤^g'''%K''#Q.U@-|$`N:xӰ/a/O@ڍGɲC&@`>!r9Ir|b ]51t~}1WPZuMBX4l 0hb|[2|t|l 8N&& pOSz4$4$! 'X g 8hCB&:+qﳉ}\tLcg8訿GH>6>7tlwY8Ɵ4 Q[uBWTI&MjbjjtU|WvX2k^ 7(?2䤤ާ%%%%9)))/d!zkġ+$ T ; ռ\K{$C^6FAнӐz|lt ӈx_C&!(K'{٬II?iPh?`d6666ccc`^8'EEEA=N&&&& q00OSz44<4a`^ 88888'|spg 8ȘED&2#qﳉ}d|Lg$$$?g8G —*Hs)G92ޢiRi/g~%Ͱx4k/ɺ8 #V {4i͊Vri Nx/]rrRRN>rRRROSz9)));k!P>%%QCU:_NX(`+C@ r222z|| 8Gq'g8'EEEA=N**** qQQOSz<<<<᠞ 8((((>hODDpѐﳍ}L5]YȢ#}{䤤ާ%%%%9)))/III@$}5@Taǐ rrrrN8R/ 1! FnTI =roSz|t|l c8EA=N.... qQQQPOzL Q.?8X8xEE*qﳎ}TTdW"b'!!!8GEF>>:?qﳍ}|_ dc"}{V 3ӖKI#K+699=9]ӭE NH4 $$%?g%%'%9)))/IIINJFJFӰOT: $p4h*q=O&qyĬ|^ ;_*-PHGϐx<8G@N::>: qOSzTTTT⢠'8'C=N pO|$࠿_mq.؏BC*Hƿ&*#qQ_ﳍ}TL|_"g#!8EFB>B?qﳐ}x"hg ΁tĢ 6JAWlȺ~ h4Pj9)I('III@NJJJKrRRR_Syq]qC)' 2dPPPT6?qQѿﳉ}|d#g!8BJ6;ܣPCx N||F\jmx40J M [|VnBD̓d g%%#%9)))(IIINJFJJJ;=%%@=i=:* >L 3N_tMAAAAn`zzz4*j 2a@*3ixBFq/Sz\\\\⢢8xx'=N" pААOSz$Xh"g 8xF?."H2&>7qQ1?ﳏ}dT|w㢣?g!#8HF>BFAroU43TU>E@A=AAqӍ<3Px4ȪIHȽﮜ $`%%#%9)))(III{qΑ{p\?]T??!uϞ dAA|$dzZޟr˅/Ѕ/\X4c#չq/sz\\\\⢢8xx'=N"" pАOSz``NBp1_ﳅ}L\2:1ާ8ȸEEEA=N**** q110OSz<3 ڭJ*p҃`4MAAA=Vi͘$|'#%##9) 2rꁠy4 kCPu9 PEl@$d$ާ!!NBBBB $#%\ ԝ >Sƞ 4o2|I:*s!k `6.6.ާ8EEEA=N***& pOSz<4<4ᡡ 8H8H8'@@=  q?ﳆ}DT4;"b?g 8xD&.*#qQﳍ}Tl\o$#?g8#!!5!:= 9/4&P VhAvh%%#%FJ3"hd>@-2ʁ\@ @-TyjJ^TzMI<0 h@ׄ ͱCS~| 9 #e!鯃{>H !A);*;i-Y8GP.21ܿ#8ȸȸEEE@***+q1110Os{<<<<᠞ 8h8h8'AA=   p0000OUt,0e 8xD?"."qQQﳈ}d|LT "cc?g8.>./r21?|tc0 k#h`/4AEEE{Ι؄-  |ZP^ BhȨ}!cL<=!k5xHgoBҤ]jjJa)*,θS KUPak "$|RcKFFFB lѳ.z{>2>t5R6J~I"DWA,?38EE****qQ1Q?s{<_OD@555ɩ(M1*VVY|˞}e_0?OH(>~%2l.| 5=p}sڣqYUQQ{ϗ>669|%D|@σ@1}t4m Udy G}r1/s{$d#^H{#Ͼi o{E_|t|llG<#scc^8'EFA=N***& qQQqPOSzDDDDᡡa` 8hhhH'AA=N p00P0Osz_4/!! _g  8xȯCB..+qp/}lTdt "`_g8'FF=B:6> qpO|td|t#c !!8'IGGA=NBBBB rOSz||t#㣠8'F=N6>22 qpOs{lT\Tb8x'DCCA= pаOSz$ࠠ`8'=VP}4</aag  8x*.+pQﳉ}lTl|""g8ȸ'EA>N>6:6 qqO{t\tlc!!8'GHA=>6>6 qOSztttt㣣8'FFA=N2... qqQPOszTLLLbb"  8XXhX'C= pPPOSz  7' _ 8(H(H'C> p0O}4<4$# g8x'>&*2. qqO}dd\\cc# '8'>>B>B qO{lttcc`8'=N6666 qOSz\\dT`8x'=N pOs{$$ 8('=M ooYh  00iA(`a_~@f `| 7H0@9 +o3$ 0P¾eH0`|Mxv`$s W4߭@I 80i`_s 0`C > `| 7 aZ(pa_~$ 0P¾}L3`0`9 +oְ`$s W4ߐ/hL0`|M 0`C >1qM`I 00i[A(pa_~! 0`| 7@0`9 +oƴ$ 0P¾lL `|}dK`0`9 + 7`0`9 + 7 b"A(pa_i(`a_i`_s 0@C %M 0@C %M`$r W(oְ`$r W(oF& 0P¾ @~6& 0P¾ @~@21 @`J 0`J1`I 80P4߭J 80P4ߐ/pP `|pP `|}lkp09 + 7`09 + 7 cہ(pa_i(pa_i _3 0@C %M 0@C %M`%r W(o`%r W(o'7( 0P¾ @~7( 0P¾ @~A=.1 @`J @`J 1J 80P4ߍI 80P4߰OYmP `|mP `|zlkp09 + 7p09 + 7b(pa_i(pa_i  0@C %M 0@C %M`$r W4߭@I 80P4ߐOyjL `|M 0`C > 1 `I 80iZ(pa_~=" 0`| 7P0`9 +o'C4& 0P¾hH0`|Mhf`$s W4߭ I 00i`  0`C > `| 7`(`a_~$ 0P¾{00@9 +o``$s W4߰OyfH0`|M 0`C > 0P,I 00iYA`|MP`$(`a_~" C > /`I 00iX(`a_~= A W4߬`H0P¾{0 00iXA`|M1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb;O }"dh 4eg8hhxx'LDLDg CC=w_8ظظ'....C8 'g޻_E=qqqqp/xg3z3zt$$$${Mݼ{pO_{φr22_;'[ !!a j?'A wnxg/pU6vqO_8HHHH˕q@ܞx??ܿ3 :N Sw:22./{t vݷ/Ŷ{Wfnܯx}n{Oz'y>󵻋оC9)))'wx3Sb?ݱmvpN%~}xf3I=O<9 <F/ۦy Z݃wh3s/ݱo{7xC{wsݷœ88@IҼ9}`ݷNLLDDYi<{2BB7^ cb`^?8,<$$GFA>n6666 FA=7 "lh 5?  !``۝vg ?o nG/ۿ\8?nk7mݭrہ)_cw_!r:Y>vݱ{9s6{ߺ.oFqYGz`ׁh~A7bAu<x"v x-}w#[N-3z8J}x}vš'G[y>Dov:2q "}'v7Ќj{nqtos^ >ED@!xCwv=x{ }},?C8A >;} c`_!s3{P:>>?c<DC@xC> }8'gq_@!n}}pgpC c#`:B>> s] Mntdlt`fnGGA>8 qOp>::: !?/Z]C@z=s{PMpMA6+-p7ww} "ph 5?xcw*&**qO^llll t8HH8X'ׁ >0g[vFGA>wx`YO'Ŷ3{3}y7}8xhqqzvHG=**&& 8?}}yG}a, F}O :g'}}O!;pJJBGu}}}-x[GH ddgspJJJJ ddd`^}9h Ŷs>FF@'y9xC'x}CNY߻o]=}}`9(O=x.&.No 4`y͓G6['T LYJJNJf70ox'}}}pNFFFؼpааO^~}NA'',EESo_}y>xL/=mw}+۴4}c[gdpOd|[/O0g{sʼn=IERR`?.IMMQfi3sr'xx(}&22))L3RRQRWݹ3z bb"`' b_!d|l۶*iۊv鶮aN~0{qOa|tt cv ᡢ!_}>::>xi'qOa|3| 㣣>3}}}[qOEE=AA=~AAp/,$,$ ﭽn}3} c#c`<3zR^`o} { L "th 5ppp/w1n>>6> ݿ?q/n,~݃zt_G9)))JJJJsmg{g%%%%ݿũӲ.tlll 9?{Y͹B4ř׏6262tݫ^-_ 㣣#޿޿ݯ}IIH|{ov\dLWg%%%% ddd^2oYv8Șۂ%[ᜄˈnW8'{+ڳηz@ᜄ||'onм:q9) (+ޘݺ_xW+C8gLLLL<_g%8(HH8'.qqOppppO_pmn[W ?,"xh 5m1?9 x43|8 ࠡ o//p+ `8g  x%ݯ3} ?<_ž>D #y}mn}8ׂo}~9q޿o=np~} =ޯ~y8{1wr2/'gW=?_vx0g  5=P <}xKT\\_-q/Mo x,3}}߸qqOY4ws ":::: :sko}O8xǀ83{koy9}LnǙ9y<3pq/xc?!!ldllk'> b #cHHHa Gqy>%xCp6:>:3{ d$$^9 ǻyx&3}p:g#%!#H@ݓ}}pϾIH^IoþgφX m3JMKK\}}qq 7{,͍ϾaP3MMMNZ Y;o=r}nWܯ@9p3}x^{}}}}G|[[y28f971 7Mn}OĢa9A9{uzlo7>tx TU Tf9o;?Ů;y>%>Uߺ&瓷{Ǵv?9Zt\lo'}j>:t5w%la';s>WzTy߸NrpLLDW8>gpGnyHXh7B7pE@}m7'>^>_9('fC@p\L xxtlttLnkrۭ˕u[[sw;ko n"h 54c  (i ~ h''Z# C7\3} C?'   3{pppppOa,4,4CpoLDLLgO'A>7g p0pPpO,$ g 8'3{*.*. 3z ᡡ13~~ `?NPg CD@q/og3{ ⢢8' cb87.... ?A>oO/$$, g p20&p00OaTLT\ g "T\\\8'_8(8ǀC s'pyp1/aRsg3} c"?C8***+p8ظ>::;=;[sq/06666 DDLL|3}ss@*'3| "c?8׀ gFFq{}sg FEFqQQLLD?㄀#qqqgxg#!%#,$ ^8hhx6:2:3| bqѱG,3| 㣤c^8N !̞n @gEqqaT\\_` 9/tϾW822:73| c#pJv/ᜌ́'<_q111l|dg#FF?< qѱo $d?ރ9 4;D_pqq>B.53}p0qt,nb9)))/pb,lldg3\3}x<8+\y; ް**** }o%8>2B?FGr2`['=כ}/?62623zW&*6/ 8H s,7ő=؜'}<qq'8 H۞/owvXgȏ7o F8ct}}؜8ᚂzӲs G?xtTlsg<^G&

    NӚng8Xhxoghy< 3}x9`g!!p3x4W8K]sxP{{&&&& A"&!> :6:9X3|))(f4'h[O}4Piq}r02.>;!adtlgض((M UUV&I$8'ȟr%܉ȟyP ԔՕ3xy:OO;dtlo_y?y9'l$>J:53{ $d$ ^WݿgCA=p ,/&3}y>GF0B6B?P HGqёOatlt g!GGGq?t|oۿC=  0(x3}xK ⢣?\1}g!q0/att|wg#8'gqѱ/_pOnp0&.&.3}x~ qOp>>:> 3| #\ 8s-Yks[{[w  Z"h 6c C7f `a $gCCCA=8HHHH' a8xxx78x'"""" # a 3} a,$,$ g B@8hXxg 8' 8hXxho0Op 3}x8xxx3{p%8'qQQqpO?. xpа/DL>BB v?8('j_pp04g  GEGqa\dd_g #_8ؿ  4<,?g BB@LT4GqQqQ_altdg!!GF^P:>>?3| $,$g W}Dqd\|ogGFGq1o/ݯm('AW}qqQѿ\lTSg! Gq|lg#Gn?=ڹ5W|3}} #C8ع26.-3} d#C8%%-NwWNW `_}8 P2:*+3} dcC8B>>G3}oi;M?mvI ccc?޿@ ⢢b_}ܯqrL\dWg!!!dttwg%'NC>Ǎt1er+g#HGqqlg!#!FNGq~Nkq_}+ b"C9 B6F?3} $$?C9)BF:;s9955ni;_}} C8ȟ>B.?3} ccC9gxS'&,M<_q dbC82::3C96B:;3{ d?~{5 w}&*.'3}r+3} #ca|Ӑ/}9)nb᯾8 }66673}x7_D;{mϾþtlwg+vg!}'}};3| $#8 _r;5s+osn !!?߯s' #c/W p=IAIbisnƞ-Ɲ7k r}ATd\_wFqqo'EMEMnM ;5EE558=?qQ_'}2*:3^W#r 5E%%r'v?^DDD9+w>`inrg|3|rP22>7Fq/>>>73{ #cgFGx&7.Wgqtl|t }pJ}\0qOp66:6 GGA=`aЌpsPp w e "h 8?ooϏp/ >8H8XX'BA=&*& bbb^?C88((H  ᡡa_?.2./@OO瀈g  D@p/gfI&3{8XhH_xp.&./=\\lg3z(?@A=p&|3|xxC8Șφqqqqq/_qQQQP/H }8Mll\o3{s ^}"&#oqqqqqqq3{s'es\3z8(?}8ȸ` f_`!}} =q~N g}s?83z ߟ}8ب0o!Ó 㣣_=q\dqO@gWݧ$/_}}ss}rAlttwCޮ[Wk}}߸]|y\_lllks;J=qܯw;'8v'}2s;npwn}%8gpg%%#%?'{Gx.B]3}I@sWkwyE _x} cd ^1;N1C9z4447/-}̟}.W߻[]w}M0_}r2 5dh\Oޞx x }xCHFqoo[v_7xK^Nwdܮ_3z;߸k\g%!;7|3} #c?_}x8B>:B}}Cr8ȨȨ88X(8"n}:6.;ܮa.W r"h 8?A>8((H'g8HXXX' b"b`^DDD@ 3|o?/ g  ! _?޾o g @p000/, 3| !!`^ 3| ``_C88H83} !! _p ߿_7 3| `C88h?"3} aa`_C8xxgEDD8ȿC?((3} aaC8X8xX3| !_8hhhh"*"gz4GGG@g  A@p0Pp0/$L?gCp\\TW DEC8.../q/[Ӈ<3x ⢢`^g xgCABp/$$?$<7g  FCq0_DDD?gGǿq1QQ?l|Tcg"8/wb_L\4Og CBqQOdLlcgG?qђd\lSg!|t3{-̿>ww /,$g FEp\|:;3{q,Ʈdg_DCppdL\cg EqqLTdGgHq(g#!GC9))/BFBBs &z?"3} a"a gCEqq1|dlwgGEFr2_+ ᜔|N42CA3~ "#?@gFGq1?d|!yy2CBA3~ cC8xx&6/3} bb`_C82*>33} "C86B2;GF9d⹓o_ 0D4DGg  DppLl,OgEDCqq\L|cgGG?qёtdg!Xf5݋y<4$,$ g  p0ddLog GDqQ7g aa _^LDфq,q, {`W{`WpO_K8*,_K8*,yy|0X)Ub0X)Uc^f7ٿ@@> p0O|!!` 8HXXX'CB= p/s|LDDL""b`^8DN"&&& q/SzDLLLb"b 8x'DDD@pOs{,,4$aaa` 8H8XH'A= 00Os{ ߟ7'== oO/} ߿_g 8X88hCB p/{$,, aa_'8x'DD=&"&& q110Os{DDTObbbg8E@N&*&* q11qPOS{LLLT""8xx'DA="pOs{4,4, a 8(8'@= ?Os~~~{^ߟ`#ߠ_&8(?(Ap00P/},,D?aaa`_' 8xDD@&"& q11Os{LLTTb8EN*... qqqqp/3yLT\\⢢g8'ED="&**q111/s{DA= 'N o}$$,4 _g  8xXhDNq0s{TD\T b"`^8FF...2qQQSzddll ##bg8'FA=N**.* qqQp/Sz\T\\ b^8DD="" pOs{4444 ᡠ 8(8H8'?= Os~{_ _'8(HHNpаﳆ}$,,4 ba_g8xxDDD.**/qQ1qs{Td\gc## ^8N6667qqqszldll#c 8ȸFN...3qqp/SzTTTTbb8x'DDDA= pOs{$,``7'?= Gpp}$4/!!_g8xhCN"&"&qQ/|L\TWb8F>>::qѱSy7!YS zX+F/[Fƿ=66:6q/Szllll ccc ^8ȸFF@N.&.*qQQQP/szLLDL""b`8hhxx'C  pPPpP/s{ ^_` 888(DA"'p0ﳄ}L\Lgc?g8F@N2&2'q11sz|ttwcc8q^*>{=,K>dTFzy/Uc|FƼ=qѱ3y|||cc'8EE2223q/3{TTTL b`g8C pOs{`?'8_OCN  pqPﳊ}D<\G!g8DDN&.&/qs{\l\occ8υl u=ȩEl 4TDDUR6м2eq! e8G>>>?qyt|t| cccg8ȸȸ&&&&q1110O3yDDDG!"?  8XhXhB@  pPPP_s{ ܐpOφ}  !`' 8xXoDB.."+qQ1q?ﳈ}D\7qѱsz|㣣ޝ14hOS>ݣ9QQR+督+`>E(OHH7qqp/3z\T\T bbb`^8C@ps{$,$ ࠠ^7A@N"#pp0}LDB>Cqxtt|t c'8ȸ'.*.*qQQQP/s{LDLO"? 8hXh_AAA= pP0P?s{OӅ~<<:>BqѲ/sz|||$dާ#!9'INFBJBr2/3y| d`^g!!8FN62>7qpOSz\T\\ b`^8DDCpа/Sz$ `WppPӅ}LD<\ `?g  8ȘDF**+qQs{\\\Wc'8G>B:GqR?Szd$^g#%%!9)) (HINFBJGr220/Sz$?'!8FN6.6-qqqszLL\\bb^8x'CDDA="pапSzdta_ߧ 8HhHXD*:/qQﳌ}dd|wcb'!8EN6>.?r/{tt 䤤$ާ#!#%,t@?B'JBJKrRRP/Sy $d`^%%##9 )('HG@HF ~>>#h9x!ӏ(up uJ* #DJ$\[c󍌍z\\\\ "b`^8DDN"pаа/Sz `_d|7`g8h26"7qQQﳉ}||dgcd$'86:B;qz{$$#_^C,@:m "Ze,}i2luѡ$d'%%%&9))9/IIINJJKr23zw!vTJP2x= ^(JNibZZZRҗ6;w%8ظN.*.3q11qoS{LDLL a!ާ8xhx_BCBN  pPPP_@qpӇ},D,+"g8xwFF?6.:5q1QQOﳏ|tcg!9 FN>N>?̌t| #{h/*> TStii鉉l'8fS{䤤ާ%##%9G@6<RΥ^i翃@ m55g<A{v3Ε'ϤϞb@2%1%k<~Pe\=|_ 8ȷFEN.**+qQ1QP/szLS/iƼl\lkc#c?g8EEEN***+q00Sz4,/󃄄}<\4G"'  8ؘF&"2%qﳏ}\\\_d#$?g#%!8%{IqQP>h }{lY:H8L8> $@4&.LʘUm )IIIA=NJJJIrRRR_SzBFNT;ը2LSrc_+Nb/4^iiG!!! B>B?qS~z|l|lccc_ާ8ȸEEE@N***+q1?Sz<<>>Cqѿﳎ}tqЧcg'"9)/cr %ƣ@f5559qO.s8b|fIIINJJJMrRRR_Sy/$>t{84/fL<9m\82QrVQ盃?N:B>CrS~zllloc#cާ8ȸN&*&*q11Sz<<{٧:]OQPPYp\ OPztQgپP2pHq1吏g8FF@N22..qqqQP/SyTTTLbbbާ8xxxCNpPPp_ﳂ}0Q?ӄ},L7g8؈F"*2'q1_ﳐ}ddt_##g!#!8FNF>JEq/[Pb.4IsNNPPYlMEQaPdJJFISz$ާ!!%%G#~{UW'8Zl|tTf6FC$s> $]L2d=]8FF?2../qqqqSzTLDO"b!ާ8xxxoNppPP?s{;8xEDN*qёﳈ}T|DOcg8GH226/qrﳏ|lt#! @CYQD(RVh䝁|ipr2R23z 䤤ާ%#%#9))/9@!yʒ|WL @"j )SSONPPVzzrrԳ_ ]262-s|\\\_bާ8NpSz$ `$ 4 g  8hC?".qQﳊ}Ll`g\Ȣ}EeOM=9A=^djI4*qQq!8EE@N****q10Sz<<=rRR2?Sz|?;* 4nΊz5%KO]Vi9ʼn'3w 8EN***+qQQQ_Sz4<B.?q||| WhGNo 9)(),ηRrM~my!9 rRR0/3{7#z Ld>@)*<'7IQ$d$^9 'HHHNBBB={4@]o8fddT[ާ8EEN**&+q111/Sz<4<4 !?ާ 8H8H?@@('#pPﳋ}ZJJJUUMMr`{Kܷ&IȺosx||# ^Ʋ9SjT%aO1; b~F2*2+q1Q1P/s{LLLL""" ^ 8xhxoBBNpppszQ~LLDOb_g8Ș@2:&.qqﳉ}|||{bbg!9 NJF:K|rEQU=I_5%555Y5~>-G "vP>`@--<~qj?yYu> *<+3A+U~?8g 8X(㠼=x`dl~mQ1|3s]|#8EE****q1?sz<<<<ᡡާ 8XHXOA  pP0P0OSz, ?b!a?ߧ 8hECC2**7p1Qﳌ}d\lgb_g 8ȷGEN>2B?qQwӔTS)gQYEQZbjj jjNV~y~@Fq52? NJ>F9sԨ3x 4jj.UUg8A "(FP #,5s Qb̔#4p>*5Άpqӎ}{ldlo##b^8=N&&"& pOSz444, !a ^ 8(8(('@A= ppа/ӊ~,<<7!g 8hhF""2qqﳋ}\tL_cd'8F{؇?Q=7'\AMT5CY>  (zH<~|dd_#=%!j# 4P3r|be@!ʢ04 ~/L;TYh?a%c!Mq  y 0t|{FF6667qp/Sz\\TW⢢^8xx'CCCA=N pааOSz$$$ࠠ?'N&qQ?}\dDoa?g 8E>:.6qqﳌ|tdtt b`_'!8Ɛ{< \{GƿNBB>Br/||cޣ?# 8GG?>6>:q/zlldob# ^8N&&"& qOSz44,, !!?ާ 8H((('@@= oOS~4,DGag8HxhOE&"*"qq0/}D\\D 㣣$g8FHGN.:.3r?}|d| cd$ _'8BB6?qSz|||t 㣿ާ8N222/qqqs{TTTTbb`g8hxxx'BB=pSz ```7'AN""p}T4LW"ag8X6.>3q1ﳍ|D\lgc`_'8FG.:67r0/s{tld#!9GGNB>:;qSztldd c#"^8EED=N*&&" q11OSy<4<4ᡡa` 8XH88AA=N  ?Os{a a?g 8XHhD&q0Qﳄ}\TD\ ""_g8DD:6:6qQQQ/|TLlW#`^8N:6:7rs||tt|㠞8'A=N2.22 qqqPOs{\TLTb 8xx'CA=N pOs{ࠠ`8'M0 D ZiAAL @f$T 7H0BH)VoB3%je$K0*Mx`"SP4@E Zi_ P `U$T 7 aYAL @4$j}T;`0BH)Vo`"SP4/h(I0*M P `U1qM@E ZiAAL @@%$T 7@0BH)VoG5$jo(I0*Mv`"SP4@E Zi_s P `U$T 7 c"[AL @6$j}TcH0BH)Vo`"SP4/l(I0*M P `U11`E ZiAL @@f>=$T 7x0BH)VoŶ$jl(I0*M`"SP4E Zi_s P `U$T 7 cہAL @7$j||cx0BH)Vo`"SP4/m(I0*M P `U1E ZiAL @@.1$T 7`0BH)VoF7$jm(I0*Mȶ`"SP4̀E Zi^ P `U$T 7 bAAL @5$j{TCX0BH)Vo`"SP4/yj(I0*M P `U1@E ZiAAL @@"$T 7H0BH)VoC4$jh(I0*Mhf`"SP4 E Zi^  H `U$T 7 `AL @2$j{00"H)Vo``"DSP4/ye$K0*-e$K0*-{ (0"X)Wk 0"X)Wl `_AL YL `^ H `UZ H `U[0`"DS0`"DS>%~%~@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb?4  "h :? 3}}$ ' ᡡag8 __C8'( 3} _C8XX8o"BB@p0/qqqqpO _ߠ_8(OO3} !?C8xHo&&3{qQp0/qqq!\\lo3}|p00п  g CA@p/$,,4 gDE@p c##?lldgA@o/$4$ g AB8xh&&3} bb"`^8&".+F>>6;o $$ gCq?4$L, g qQQp/*.**3||ttt ߺN5x<g# q&.&.3{ ##"8q7I8ޟ}8x׀ g "?8:6>7 $ ^^';3{g xC8ϸrFGF@q8Hg?}}x<g!v'}}}3{87r9))g3{ $ ^9 ;Nq11q_?}}}xn1:$N;ſ '}}}F>FGne;'݉#}}t]'C4ũ׬3z݊qq1_WxK%ݱfg.NG}CqFFBA۵}}þOOrRr/3MONNZv ?y}ȿn݉nN>x\^sOv.}y<n݉Ɯ-ݯ]pg}(',b}ܯB}}P ^:OA۱{>}}&0(SbtNiWb}_3{ }}}}ڸ'qiںnӟ_}}}} >'i7K}}}x tؿ}} cb_}:*>7(3{q0/0My3}}ol\|o۠<  ccc`^} }7o FGF< 7"h ;?0?3{,4<4 """ ^T\T\  __a 3}pPPpp/""3| p000/"3|8xh?C8HHXx3}8HXxh@ $ !aa_p"#3{7g Cpp/DDg;NpJ}}2W+H'i}}}}C87q>y' Ww'}W;}}p`n-ѹ/y3{3ztdt3{}}}x  X"h KoB8^gw}tݯ}}[}|3}p3wv}}Ippﯾ}}~o fy"h ;pppp3{8xx'&&*3|||} !`_?3}~|pP"&**.*.3#q1qp/ a!0*&&3qq1 a!a <t']p6.>53}r/\llo%܃=u}}2:&13} bC8g!rr7}&&2%3} c$?C8ؿIҺEg!FEqq|||gᜤܯ_P"6#3| cC8ؿ:>*7GF}8*":'3} #C8׀OܕLlDOgDqrQ% c+/׽qq1_Ll;3} d_}[ޟs|3} b#"?C86.>93}}߼q7wp."&/ !C8"**'3} bc\ 㣣c޿\d<_gGC@qQ/D\\D gE<} GFHF}8Xxh_26"33} #!"`_C8ة"2"3|} #ᜄ| T@%8g #?޿g!#!h&;~GGi{ᜌ&rR2R_O,/p88XXO}}} IH@<~}ONW˘w}v9/{/ܯ }}} nӒ/t}}6*23}}8NBJE3|//....g}}}.@u?qq/_W_}}~o xv>b|L,7_}}|3}}% ;}}}nuu%!:***+}}nf4JNFI7Ic>}}~tr+}}}ߺ4yQ]IQf]įܯ}}x\P)):`3RRTSWr]_ }}} c}}F>>>3|>B:?o };3| cc?pwL  "h ;3|3{DLB2CF@d\TwqѱB>:=7^~zP:666o?~}}98/Gzuut ޿ <\TGx x/[Ӌg>g%#%$xkt ?7w#w3{ 99=9bm_r<3{t}i _ _~F\FFFC x}_r-}n~))+Mn/p/6O a?==FB>G||t| pPP3{8z}x O "h ;gg 8xh'D?@C7?g  8g  3}3~|0_/3}q10/"# _a,< $ g  DE***/ c"?[]HgFEC8HHx "^^C8Ș/ <3|/}t<3|'{xg!!!!}}8t "^_}IIbt_'d]7}o!||tOs}~r~o}~O~<};ܓMN}}}xq[}}~HGFn}}3}ص}}a\tto?x 3r+}{~Gq#?}}.6:7 㣿+yKt~B6ttTbPMQEMZnz& GG޿&&&& {qёTTdWg!8BB>BgEq110/P.*6+}8hׁP*:*3} b _C8g "??Z B"h ;3}~~}|  p3~|o?/ / g   ``_!<FFFG}}:6>?/_} $GFFFGz..*.g # _=%/W/+|3}}} dc$?atlk}+%}}n]};rR2_||wҺ'G}y>$3|[,ogx5!{o®G+}}8ltg9'_} dbc?C9)׀KXg%!$GHGur}(gH9gw{|3}pH^Ko?%}~%u?t_||t{w~555=9]_}} _}8دuy}|d˺z_Kq_ dd ^}|3}xw} d#??xCw}}x 66:; c"h ;f3{f **"&DD o< O ᠡ?_ gqd3}q1q1QQ/_{ b"j***+3Xg''%(޿o__UQUUZs_/8ؿ 2h  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb`~K<,] x,|g|gOP/a~K<,d~K<,}~0#2Yc 0#2Yg   ?_h  xX  xXFP`>Gd``>GdH3?%3?%BQ,,|g|gpp/g~K<,i~K<,}80#2Yc00#2Yd44   ! _h  xX  xY F`>GdƐ`>Gdh5?%4?%D@Ѥ,," |g |g/tj~K<,k~K<,{P02YcP02YdLDA  "b ^  xX  xYF`>dư`>dȈ5?%5?%DD@Ѩ,,"@|g@|g/tj~K<,j~K<,{@0#2Yc@0#2Yd<<   !^  xX  xY Fp`>Gdp`>GdXH3?%3?%Bќ,,|g|gp/tf~K<,f~K<,{00#2Yc00#2Yd A A ``^  xX  xYF@`>GdP`>Gd2?%1?%ш,,`|g |g/ta~K<,`~K<,|{|0#2Yb0#2Yc   _&7پ?>M pPP0p/} ^ 888XXBBN"qQ0Os{4D4< "b"`8N&*..qQ1QPOSzDLLL bbb`8D=&&p/s{<4D<b!^ 8XHXXAA= pppppOs{ ``7'>?A= oooOPOs{~{} ߠ_&7O(AC@pPp/|<>>Mp0OP/}$`_g 8hXXBB@Np/s{>::q/Syllll #ccg8F@N****qQQQ_SzDDDG"!ާ 8xhhX@ppppP/s{ ߟߠ^}|# a_88X((*pпﳊ}\T\_""'8ظEFE*.*/qqq{tltwccc?ޥ?*CR#tuz4t4463e $TKGG8F@N>>>?qSy||twcccg8EFE*.*.qQQQP/3yDDDD """ ^  8XhXo  pP0P_s{Pn}}~$4 !?'  88hX?ECD..'p0ﳊ}ldlob"'8N6667qёs{|||wހ*.)OR憈;*"2*٦{g 7NB>B=r3z|||㣠^g8F2626qqs{\\\\ bb^g8xCCpА/s{$$^W0ﳄ|>B>q/Sy|||㣠^g8N6*6*q1Q1P/s{LDLL a" ^8xXxoCCC pPPpP/s{ܟpn~,447ࡠg 8hBD"#q1Q_ﳍ}LT\_b'8GFN::6?qsz||t$$d`_#?ߒ3NCW=|d$?g!#!9 HHNBBBGr22?ytt|t ާ8.*..qQQQP/s{DDLO!"? 8hXhoABpp0s{3'`ߧ 8h8xB..)qq1ﳈ}ddDO㣣'8踿GN66>7q/{ #?ާ!###9)(GA=NFJFGr22_sz ddd`^g####9 HH>>BAqSytl|w##c?8EEN&&&*q/s{4B.Cr{|dާ!!!!9) ?HFBJKrRRP/3zddd`^'##%#9 NB>>B qd#_:::=q/zTT\_^8xDD""pап|$`g  8xBDCNqQﳍ}4DLK#bbg!8EN>B6?qq{l| #$?!99 'H\'G`"䤤'%%%$9)))(III@NBBJJrRR2?3{cødzgM B&Jn=}4A%3>ќGjIIG=C(:d\d[#?ާ8D"""q/s|4,$ _'$4ߧ 8XEEqQﳍ}LLdS㢣?g! 8ȨGEBBBCq||c`^cc|OD5 YkyJ'SXLvڃ~m䤤ާ%%'%9)))/IINBBBCY (?: $RAWY'%/)%-1j}S@4{8ǩF@N...-qqQQP/szLB>3܄4 Jbd&&%?4ɦ''NV _|s%%'$99))/IINJFJKr2222B`d!{7-{Di(S1~>yï-R>=|-s p sf~88*'N:665qSzTLTT bbb`^8xxxCNpPp|4,`ߧ8hHoE622;q11?ﳉ}|ltwcg8 GF6B6CY xh_R Fzj{7)ɯ~zfCMMKL[rzbzVh$.M.dާ%%%%9)))/III@0Nj%DzfkҮB 䜃7HNB>B?qQBLt|kccc'8FFFN***+q1Q1SzD66?qQ/ﳉ}|||##g!9)ƾ^5QYMO P3w>^EH0h4˦9[fܔc8NNJJJrRRRO3zb"K~rzrs6?q{ldlg?ާ8N&&"'pS|4$4$ !! _'$,L;ߧ8HHoE2.&7q0ѱﳊ}|tdw⢣bg9GF6B27r1|t|$Mȭ5(i,3rzzzt OonEd9)))/IINNJJKrRRR%ځfӓ)*9ArmΟǭPLt#!!8FGN:2>3qqSz\\\\ ⢢^g8Npаа/s|  0~$D b' 8xxFE*>/qQ_}d\|occg###8IFNJFFGr2RzkI(8<3rzrzuh~m99)9'JJNKrRRR_SzyFh$>)9599fn4V(&SUtYHzG}t|d cccާ8ȸEN**&*qQQQ?3zDDD?!ާ  8hXh_BAN  'AN&*'pP}L4dOag8EFB:Cqrﳌ}t|㣤#'%#9) 9'HFiIt)g=9A=ZuG=`,2䤤ާ%''%99))/HH!L~z,{7HNNNM[jrzzӓӖf+9P NP`=$}lkg(X?B.7r{t||sy`EH')CITUUZ@r-!+###!9))?IINJJJKrR22P/3zJS|in"((+OazrӓSӖK8+rr/9tdlc'8ȿN.***q1PSz<<+555ꍠy'"HGF>=3z#d$$ %shx42Ru{'~m?^8ȸȿEEN2*2*qQQQ?3z<<<< !ާ  8hXh_BBBpP0P0/s{$4"!ߧ 8hhoD".*pP/}LLLOc_g8N2627q FKMMMMVjj*ʊKS-!xWI2C7hC8žj "*5QϚn*(*L51=yL0=@ {<*9 Eڢz !>|3u|llloc# ^g8ȨȨDD&""#pSz<4<7a!a?ާ  88(8(AA@N pP1ӄ~<\,O⡢g  8بDD2>&/qQﳉ}|d{#_'8Gw5Q'RPTPa 1* ֥ݪ53y}a(z ZH8r|ЯFY)UQUU^f!MQYRUTNH6X>@. q'!Bƻltlާ8EEE@N*"*q/Sz4444 a`^ 8H8X(AA@  p?00Os~<4BG8 :/һ`f gyڊh?`f17P&69`G=zddާ%% m3y~bϵLQ ~A$1'<~|tc`8ظN.*2+q1110/SzLDLL" 8hXhX'BBA=N pPPP0Os{ܐP0/~D+""?g  8ȈCFE"&#qqqﳊ}ltTo"g8N6B.>qёs|}|lcc_'!!!9 HJ>BAqOsz||| 㣣ާ8'N226/qq/Sz\TLL b`^8x'CCA=NpSz``7'AN&pﳈ},$<'!g8xxE@*2*&qQﳋ|dtL_#'8E6B66q0/|tt$$#9GN>>B>qzldlocccާ8FEA=*&&& q111O3y<<<<aa` 8HHXOABAN  pP000Osz  8hHxDBB@&"+pПﳊ}T;3zi>pP0/? #޿ $$#q1_|||gGFF>8盶n} #!C82.:33| $#cC8xݕqo}}*&2!3} #C8.6.53}}qWev  "|3} #b_C8Ȉ>B>C3} d"_~N&C4ũ5HhgqQ_ϾLl4Og!Fqq1||g;;s73~ " `ߟ}8*:&+3} "#C8JB>G`BBBAoB>gGDDqQ\TogHqrd\oWOt(w@¿q0q?ϾqpDdL?gGqqQ_tg!51|3{z' "!_C82*>33} #cC8.:.+3}}ݐf4q|3} !C8.*:/3} #C8Ȩ2:./ߺwwiTlDWgECqQtd|wgHGGqqﯾg!#݆ogp1TtDWgqQl\ogGH>ttg''#&}oG|3} ᢢ _C8h6&:33} C86>*/sL=vvpFCCqqQ/DLTL gEGq1qo|dwg!ldlgvܟwy8hhho}8x.3} b#"C8ؘ62>=3} 㣣}ݍy} ""C8X"&#3} b"C8舷2.:;3|};s/}3} aC8x:.673} 㣢C8|3{ b$#_|3} !?C8h*"2.3} c`_C8:6.;A\ttog!q4L$/g qqLpﯿ c"_W3z ^pp/}1T3|8n}pg<q/xg!!!3{7}}}}>ݿzTLTL OJJJO 7}~iiSݧ~}}}v_}}}i}}}qݿ}|3|}}]}ܯϾᅥr2RON_}o3|wݿ׽~}}߻v}n;}ۦ >w TTUޯnLLLT }_w}}xqltlgq~=}}FFF}}}3{ c# _?߾ ##c^3} aaaT4\L gEEFq1Pᅥqqq/p22:3g C"́h =?f?>C8?(83|p/p3{ !⠞8hhh3{q1110/_3~o//AA@pА/DDDL 3|73} a a _C8hXX_3|8pp3}x^8xhEd\\_GGFdddd pА/g `/ @gq/.../ݻqN;  ###0F:::qWwx{}qqQ_glltw bbb`^}} ##?? d$#^!L\T_g9'}}x<=ݧ}}߼={'{}}}}ݷP>>>?g}}|3|x;9ݿw}}}xgg%%'$wݯᅥr11JFFEۼ?{G}vqQq>6NCv׻NyG~}vwrR?rn}ݮ!g?q}}2&:-|td!qv|3|xC8>6.=}}}8Ȩׁ|3||3|8g!!qQp/rN | "Ёh =g  ⢢^"8HXhh b#b_8hxh5LLTO'_޿ݯrؿ;8_GGGqO9 '_zvܮk((o_[ӥz>LDDOᜄ|g!!ᜄt b  +"ԁh =?uz+b'zt_s_ܿz}zOW_zztrDB@ M"؁h >.93}ܿ d$?x#᜜|rnlt|wg'Ir/u}z $#>'2tݯ3|aӊo ED?tzyg ldToq/GCd\l_:62:HDDCGE n"܁h ;?gyg3}9)t''',k=fᜤM3}rRRR__srQB>6? c??MUMM^o[o bcxg~soo22&79ߺݷo,$D/8q/qqqѿg!g cb|3|g!!!3}g!FGF &"h ;? $82 s[82 9=s9=s9Os9Os[9Os79MSs99MSs79=s9MOs9G[r)nr/79""&&3~ b_8.2.33} ccc8::>>3{ _8>>>B 3{ $d$^9)(FFJJ3{ dd^9)(FFJJ3z d^9)('FBJJ 3{ dd9FFFJ3{ ##8:6>:3{ ##c 8'&&.* 3{ bb8'" 3{ a!8H(XH' 3} bbC8...63} ccc_C86:6:3| ^8 >>BB3| $$$ ^9 ) /JJJK3{ 䤤9))(JJJJ3z % _9))I(JJJR 3z dd9)))('JFJJ 3{ ddd 9 'B>B>3{ c^8'2.66 3{ "8'"& 3{ ᡡ8hXhX' 3{ bbb8....3} cccC8>:>?3} _8 >BBB3{ $$$ 9 BJJK3{ 䤤9)))(JJNS3z $9II)(RJNS3y 䤤C9)))('JJJJ 3{ 䤤^9 )(BBFB3{ #8'6666 3{ 8'*&*& 3{ a8hhxh' 3{ ࠠ8*...3| b`_C86::>3} _C8BBBB3{ $$$ ^9 BFFJ3{ 䤤9))9?JJNN3z %$ޖF?rRRa g%%''IIrRR/A g%%%%III@r22Oa| g!!GFqalllogqQ1qp/aTLTLgDCA=pOa4,44g qqqq\\\d gGq|||g!@r/ag!!##HrRRR_g%'%'JJJ@rA g''+)FNNS3{ %$ރ9IIIORRRS3z 䤤ރ9)))(FBJF3z ##866:;3{ 8..**3{ "b 8x' 3{ !!! 8.../3| cccC8>>>>3| $$$ _9 BBBF3| 䤤_9)))/JJJK3{ ^9IIIORRRS3z $c ^Ԁ e#d_C9III'շqqp0oTg))))IIIrRRR_! g%%%%HIHr/A|gFqqqA\\\_gDE@q1Q/aDDDD g BC@p/a\\\_gFFq/t|lgrg#%#%IIIrRRRP/g%%%%IIrag))))0Ldg!B=Adg)))(rRRR_!g%%%%HHr11a|||| g7xwBr2QQ/# ޛ!C7@JRJW3z %%eC9I9IONJNJ3z $ ^C9 B>B>3{ ^866663{ 8&"&&3{ 8hhho.../3} #c#_C8>>>A3} $$$?C9 JJJK3| 䤤9)))(JJJK3| %%?9IYI_ZZZ[3{ e%dC;a`7G( 3uu `aaxfRVVW3z %%%?9II9ONJNJ3z d^C9 )FBBC3z ^866673{ 8&&&'3{ ᡡ8hhho...-3~ #ccC8>B>C3} $$$C9 FFBC3| 䤤9)))/JJJK3| %%% _9IYIXZZZZ3{ %1^lKJʿ=rA g+)))JJJrrRr_Ag%'%%IHr!gq/!d\d\ gEE@q111?LDLD g CCCqqqqdldogq|g!!!!HHHrR_g%%%%IIIrRRR_g)+)+JJrag----Krag--+,Jra g----JJJr!g%%%%IIIrR2R?Ag!!!!q/AlltogqqQqaLLTWg CCp\\\_gGq/|||gHHH?r2g%%%%IIrRRR_g%'%'JJ@rag---,KKKrg++-,KK?rҰ/ g----KKKrAg'%'%IIIrRRR_Ag!!!!GHqAll|gEqqqQkauzg CCC@qqqqdl\ggGGq|g!!!!HHH?9))'NJJI3} 䤤C9)9)?RRRS3{ %%% _9iiioZZZ[3| 奥ރ9iiioZZZW3| eee9iiioZZZ[3z %%%?ރ99)I?JJNK3z $?ރ9 B>>C3z 㣿ރ862623z  Ey'o ᡡ8hhho.../3~ #c#8>B:=3} $d$C9  JJFIIHȿrRRR_g%'%'JIɿrrAg--+-KJ?rg----JJrg----KKJrAg))')IIrRR2_Ag!qAldtogFF@qqqMog{DDD?g qltd[g #dd?C93|[xg!!RVBM3|xC9)(hONV 1V^6Q3~| 奥9iiioZ^VKmozf@9II9GRJNI3{ᜄlkafEqq^@!nxgXWp<y2>DП|tg y rY`*s8g2:7o;_W=!kC9g 9{u>0ۏ7ǭO':?Arrrϡ8g73} ޝCw7gC$dt[g%#'% ^=GwĆGw?A>AE珃@ aJJJ+ᛛxghׅ >(7rRop!#0e%gF{Xj]H̡' { ߞgó'7=QzW3| ߚ...-3~pRH PzgpF.F1 92tr0d糆hp "ᜌg#%#%B2l <3| ]_P+yw2y2ᜬg))++!tv3z d$_9)9/%2n ߃w*oVFBBC3z%ΎMwzK^&EE-!He!U+C+agFFq$?ÇoY G>C'G#_I{fo0FJJI6QMz4G+okޭRRRS`>30+* JJJrU7a|,'g|x<>h/i XDQ3~xC(58Grf~|{g!#GC942H{f&D-!HeU+}xg  \\\_gg ÇoX ^ GG?qqH n@fC9)))'FJJE7o /G*hT "տrA;V2E}xP<3z[ ØG'`7g53| ` Ϡ8A{HfEHHɾ>rrRr/aSheA>i 37/y $$$ 9{3J{ba\\\WoA4_2U_^CCagFF?++pj0 =`3}zfhgA z;7C9)))'FJFM7o [C/G*hNR "ս[9)97RRRO1CJ5f Y$ @ovP8O<5ǽņo|g')'&rd`7Ch Y7)qo}| dx%2 o|{g!nnJs])/} c"bߟ8 2g _tUG)=%~ 0ζ:P =)[t=[C/Ggi "IHȿ=Cxf2>rXfr005y|{ 9)99?RNNO7tsӿCUT28~y1S3(?;B[ #C7 NuOodTpb?7>d ї7aR|2 !Pf 'g03~{ cڜ;ly^n;_xfnO| a͡3uvs| _`_Ç#oeOq>'$t>=%8b||[K/9z1bV|a!/O~ kܞ}@ Ǯ>0]oOo$?c}p[>^} f  '.^s  bhީ8:6:73y|brQSO`WgnGԏo> a ^qQqqo":C3}p2?rJ?rdi~0g)&IrR2RA\g ''ɿq2rXg#''HIIqR2?B!Ñ:>:?3z>>?3} $$$C9 BBBAHIrRrR_Ag%%%%IIIrRRR_Ag))))JJJrAg)')'IIrRRR_Ag%%%%IIIr22Ag!!GGG@q/Adddd gFEqqqqp/ATLTT gDDD@q/a<4<4 g EDqqqqodd\_gFq|||gqBBBA3z 䤤ރ9)))/JJJK3z 䤤ރ9)))/JNJK3zrrrR_Ag'%%%IIIrRRR_Ag%%%%@r2A g!!FGqѱ/AllllgEE=qqqQPOATTTLgDDDA=qOa<<4,g qqqq\\\_gFFqѱ::6;3} C9 g!#!!H@r2R0/g%%%%IIIrRRR_Ag%%%%HIrRRRP/Ag%%%%IIIrRRR_Ag%%%%H@rA|t gGGFq/AllllgEE=qQQQ0/ALLLGgDC=qOa4,4,g Ŀqqq1?\\T_gFqqll\d gGq/|||g!HGrag%%!!IHH9))(JJJK3z 䤤9)))/JFJG3z 䤤^9)))/JJBB3z d$$?ރ9 B:>;3{ 㣣# 8'66.. 3{ ⢠8&"z ᠞8xhXX' 3z bb"?߃8*.*+3} C8.2.33}q||logq/a|g!!!!HHHa IHH9  JJBC3{ $$$ ^9 BB>>3{ 㣣`^8'2222 3z 8'&&&& 3z b"b ^8xh' 3{ !! 8'""3~ bbbC8..*/3} "?C866223} ccca||tt grA|| g!! $#ރ9 3z $# 9 'BB>> 3{ 8':::6 3z ccc 8'.... 3z ⢠8'"""" 3z ᠞8hhhh' 3z[& "h !nopSp`fwvcp"p'v ;8XK{w?q rnwwI3~zpRO(lltk8 I5{'O}媵ہp #_ww}x^q_}mk7s}}z=wݾEF?>_rUkW8g!]w]}6Z}IH>+}_y}+pRnmVk'.n7S};ᄄmuws}᜔￸?-oN[￾Z﻾};w]}M}}Nro}WW|3}| kᜄ|{ӝ;wCym dd$!9\#h "=}s9(("xg9999/wAܮ0^oO'+nf78ȸ?}}߆qQR?ArRR2?Ar? "h >?HX3ޔcϏzުw W7 4\s4\ϓ3zVyo9<[N9wu<i^Ox&qoom;ox=y90n||tor2R2/#{gDD@]C862.'F͸n0w=x߸P#pY(paq d96665}ޜw;8+"q76N;Ory9;rrW]wrww222/{srroww;ord\d_ˊﻭᜌ۝p{Jvn8xxܬos߻o]< rwn >^}￸M}opwuܮ }}8XwW`5qn;oop}pݷO}n߿x /h & !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbi x,ƀP,}H(Kd1X,Ȉ)A,q/{yR#%L`_8 PP }~RN._G,s@(KdADѰ%d AB^K .2/%X 2Y/i x,ŰCVdF@t?W |>~IJr .-@dP?I Z/i x,P,}(Kd8 xY!!GP,n x,BBVK Pc! ^y4&I@K(rh?P% Ac(U,}`(Kd8 xY#!GP,i x,\Cp?c^?VcOѨ%d B^K 2B/%X 2Y/xg x,_^贾v?ECVcAB^K .򍀡/%X 2Yp/tl x,pP,{¸!\Ȑ{~A}`_9XUob/P\̀/%X~@/%X 2Yа/]5kAbh厂z?G/!X%~b?́D*/ `^K d@Q,CVd?N""&&p11_| LOϿ}Dd\ ?g8GF2"628X3/qB{d| $`_'!!!!9 II@6.FBH.>FJr2RRP/z|b$`^#!9 HH>2>>qqs{|d|l "c ^8H8ȸ'D&&'q1_s{$ D,ᡠ_g 8HHH?AA@ q1110/|TTT\ _g8FN::6>q/{tt| $ ^!!!9 HIFFJKr22RP/s{^''%%99))(IIINJFJOr2RRP/Szdd^#!8 HH>:B>qё/s{ld\_"8D""& p/s},4, ࠡ! ^8EN*...qqqﳋ}ltlt ccg8H@>BBBr/s{ d$%%%%9)))/IIJJNSrrRR_S{ 䤤ާ%'%%99)9(IIINJJJKrRRRP/s{ $$ ^!!!8G..67qqqqs{LLLOb8xxx'CA>N pOs{LTTO_'86:::q/}|||| $$$ ^!!!!9 HHHBFFBrR_s{䤤^')))9))9OIJINJJFIrRRsz 䤤ާ%%%%9)))(III@NFBJJr2s{|㣣8&&./qQ11P/s{L>>Br0/s{d$d!!%%9 ))/IIJJJJrSz㤢_g9HGJJNNROrRRSz䤤ާ%%%%9 )HHNB>B?qs{tlto8ED&"&#qs{4,4/!!!?8EFN2627qﳍ}ltt| $$$ _'!!!!9  HNFJJJrRRR_| ^%%''9IIIOJJNVVVWrQϓ|~eeeާ))))9IIYOIINJJNOrRRR_3y $$$?ާ!!!!9FGN6667qqqS{TTTL b^8xC@p/s~\\\\ #c#g8>>>?r2?ﳐ}䤤'%%%%9)))/IIIJNJOrRrRs{e%e?')''9(GȼRRRMrRӍ}|L%%%'''''99)9/IIIJBJCr20/3{|||| ^g8FFF@*&*'q111?s{D>N ppQ_󋓒|$?g'%'%9)))/HIHBBBBrs{|||| cc`^g8.&.&q11/s{<<>>>q/s{d\d_b8Cps~ddd_cc#'8HHH?BBBAr/|䤤'%%%%9)))/IJNNRRRr/s{奥{t.n& 7Hgx)ABD<>>Ursz%%%?ާ'''%9)))'HIHNFFBGr3z|||| ccc`^8ȸȸ&&&*q111?ﳈ{<447ᡡ'8ȸFF:6:>qﳐ}$$$g!!!!9)))/IIIJJJKrRRR_|%e%))))9iiIOKKK?NR6Z[qQϯӑ|DTee)+/,9iiioKKKNRRRSr3z$?ާ!#!#9 HHHN>>>?q/Syd\d_b8DDNpﳋ~dldkcc'8BBBAr2ﳐ}䤤'%%%%9)))/IJNJRrs{奥%--+.9iYioKK@N^ZZ[rsz 奥ާ)))(9II9?INJJJKrRRSz#$?ާ82.23qqqqs|LD4Gaa  8hhhhN26.3q}|||$$?g!!!!9 HFJFIrrRROﳒ{'%)%)9IIIOJJJ@NZZV[rSz奥'---+9iYi_JNZZZ[rҿSzާ%%'%9) )HHNBBBCrSzlltwccc`^g8H9er!8xxoCCBNqqqqӋ~dldsc㣿'!9 HH?BFBAr2ﳐ|䤤'%%%%9)9)?JI?NRRRSrSz奥----9iiioJKNVVZ[rҿsz%%%?ާ))))99)9?IINJBFGr22Sz|||ާ8FF@%. X>&>Bp|\\\_"#?g8GNBBB?qﳐ~$$$?'%%%%9)))IIINJJJKrRR|e$'-/--9iiioKKK?NZZZ[r|%$))))9IIIOJJRJNKrRRR_Sz|{8N6263qq|<4L?"?ߧ8xhhh>4N&2'qqӍ~l\Kb\8TkSߧ!ȷǟߤy|tD⡞zsA"\OARRBGrqpﳋ}x!CmG ye%?+-+,9iiiO?NB2 q﯏ԇ=y1RMRZ*IrRr_}:!эǞߧ 8G׏?}< +)"cA2../qqq?sw|A3@8pпﳅ~r}g\p{u|KxFD;;nF<?Ւp3~tttk*G$Q 4\׾M7qݏH2WCrs| ͏w{$$߬.Ptrtag%%!$9Y99(2o>zbf7h^΢Z'}~~dDlCߚ&c?*pq6: 2Yq 5]_Eſ2:29qѱb #螰h3{,c C#g3G聛kC7gDN:6ArR22Os}܀>g=3p{NRBVM8h#~}$e${C(,O]%E{f 8(GǙH?+_Z vEp{||coGw%ia+ߢjĿN..*/8E>h2@] jpOг~{3;X$q|C ߟ8؈g4GA럢:g г~|tL|_i 'oZ 9 IH?F:JA2vr@d 7oOz||%e%?g)')'x7+PnW _g=*r|d-)9I(pA{pf~&qqoszZ ܶ7hk "!"9 GG-n{#$G.}T\TO⟢* H_a X 8xhxW>>U2@` *Ŀ@..*-qӍ~!C#?Ci`3W 8vAg` 0~{$c;'w$_$3zzHfiIRRJQrrGrep OF{"g!)(9IIIDr=8'_"#NM 3A{hf88'IH?.P.Z _#4W*q21R/|||w!02D ߡľ@&&*/q/o] $Zp,,a V2>Z 8FF38OV ުqѱs}A#?u`ޡ!! 9 GGww%NK HfӐ~|$䤿ޏ` 3ܞ0*qowttեtd|'*_} 7ةɾ>NBJA9h2re7&qR1r{t|]Ζ"iNE ߡ>A*2&/oΊ|wV$ g 8hxxgþþՀ2>`q &\iFCDN..*8G?@24Yq˟f~>*>ݨ2=s(dt<3|{D$#_!!!"9ȁGE ^~="rqOϳ{wrd=3={@f88ƻ:=pd>MГ~}l|GxrxdA(f88iG?+ONO a: ^z7I>::3nlwGr f׿EU/~L\L_] Gᑿ5X8ps}z.NFBBAqqok>/Oa0BF=rRRr_szs}g+!{qo󐅒|,zd ]{c폰qAuz}'Y#Ͽn>}oqϯys\ gd=>)%q3zۗ8ϿYuol>8h8gt}YIj> 8(gGBCBq`}DGOd[#ߧ 8HXFǿ*:='OGHHNBBBCq??~JCg*BJMrRRR_Sz9c? ~RSqr{L Ocd'!'#8IOHNB2BCp2?qC{Tt##ާ!8x@?88hȿ&#o},,7aaa`_' 8EN2.2-qqﳎ~|||| g8 HHH?BBBCr22|䤤ާ%%%$99))/IINJJJKrz%%%?ާ))))9I9I?IIINNNJKrrrR_Szd$d?ާ!!!!9 HHH@>:>:qS|d\d_b'8CDp/|LLLOߧ8GFN:>6?q}|||$$$!!! 9 HNBFBKrRrR_Sz䤿ާ%%%%9II9OINNRJSrr_Sz䤤ާ%%%%9)))/IIINJFFCr2Sz| ##^8ȸN....qQ1Q0/SzLDLD !!^ 8xhxh=**&'qqqqoӌ|dl\occg8>>>?q|ddd?ާ!%!%9)))/IIINJJJKrRRR_Sz䤿ާ'''#999)/INNNJKrrRR_Sz䤤$?ާ###!9 HHH@NB:B;qSz\\\_ާ8DN&"&#qOs{<<4,!!! 8E26.3q}ttlo㣣cg8HGBBBBr2/Sz 䤤%%%%9)))/IIINJJJKrRRR_3y 䤤?ާ%%!!9)) IHHNJJBArSz##^8N...3qqp/SzLTLL bbb?ާ8xxx'CA= pppO~TLLWg8ȸ6626qѱ/|||ll c!9 HGBFBGrRR2?sy 䤤d ^%%!!9)) IHHJJBCrR2Sz $$$?g!!##9 HHNBB>Cqs{tttl c"g8D=&&"&q1Sz<<<4a 8(('DN*&*+qQ1Q_ﳊ}\\\_g8F@N6667qѱs{||t#^!!!9 HHHNBBBCr2/s{$$dg!!#!9 HHNBBBCr23y| $#^8GGGA=N:2:2qqqSz\\TT bbb8xhxhCCA=N pаOs{ "!ߧ8EE..*/qqqqﳋ}\\dd _g8@6>::q/|$$$ ^!!!!9 HHHNBBBCrs{$$$ !!!!9 'HG=NB>>> qOs{tlllccc`8'=N&&** qQ110Os{D<<<᠞ 8hHXH'A@M 0 /Z 0[هjx-֠P-~DKP( l l bbZ@^ `` `_s 0[/[P-mx-Ŷ k`( l.-` [@^ `1qm/Z 0[/lx-P-}lkp( l [P [@^ |-}lkp( l l cc[@^ |-nx-1/ [P \@^ |-{|{( o/>=`P `^ 0[k( o/ypx-`Ǹ Z 0[l c@^ |-px-1/ [P \@^ |-{|{p( o/BA`P `^ 0[k( o/Ypx-`Ʒ Z 0[l c@^ |-nx-1/ l cbہ@^ `` `^ 0[/[P-mx-Ŷ kh( lf&%` Z@^ `11-`/Z 0[/ykx-ְP-{<;P( l l aA@^ `` `^ 0[ /[h6P-gx-A3 k(( llx-'<0[ l? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@C/ 6"h =^ >/݂ہ‚ͼip ҃]:N^w~w5`Fnn]cv^}xn݅; 7 {#ӂ{cpvmn7ON Ȝ䜷'm}ݧ6=؝{ݯd'߾gwM}~wwݧNw\~f}߸w}'{u}~￾۟}pT_{}}3zxKk'~o=\}qݽ{鸯 W"h =pp0:tlbs@-lîV pjnn{=Nuq!½ p ^M c{FUos$>wiWM^M;Ox;G0w9iiiOǃwo㽾_s}}yq?7v;} ?ޞ7;}{q;70fpTw=8ۍ8{wW={{}[nQ' 78'} '3piv7t ܯosfr_ᜤnۿuDLLCf}t~7cܷrq11/oǎas}ޞ}}n[Wǎ<{ #"}}᜔ӻ}~[_=,7>o߻ rO}y_ Oy"h >?S $sp7(1ёC2h_E \}Bп "c?5O8+ CnXP͢XW/pD&:.-POp=mIN "Cc0/qW7};m%@Ox!xv v7?_}8ȸȿ3{ %%%??w`x+ ;U3zセ} }o 8 $d^o x;oLL,OmpTn ldt_Ӆ%>?}C9YI9G竆Npa86+8Aa;$ی8ݽ=ѸoOW CsŇ=1|s}弞wx[]ܻܯ=a{J~wiʿ87MrpZ-_}ܻ￾˹}}ǯ G<W߾u}žr_˘;˕؜nۙnow639wO'+W}or+ !!_?Z "h ??=9]'pzZMRql_ m\NjN]=̀{-08q:0w%#Uxvp&mǻ7O:*'Uw& AJtWICˁpGq*ݏtW[w t#r. &-ywo'W}txK};ag-+-)U}}psxC}}qQQ1O ޿t?x&|3|x= x-owxxg}@&a{rpUr22 98#*q݂NS= Ѽ'} 5.spvOOqÍx]s.䜏}ra8}ww~v>}U#}sp-؝wp=\#Ύ}{ 䤿}۾wﺺr%_ '~}~v-ʱ'8[p>77y9s3zxww;}t;(g\xg>}߆r2W}{{qѱ_ᜌ 㣣޿gCCC@3z.... ? 7"h ?{W'(E{sn m̓^ 4N{!6>7(yww Oww}qQ1ώs'+ky>]p~'[\߸y_}/~~r_9IIIO2223DDDD B "h ?]G>0pۂo !y pVડfTa FsA8G8+h93N(BH5‚ 2 p߻ q݅w.s l݇wx'vM[pwct.}7;vn߽>.<][7w?'~Uao'{ssaoo77ᜤtNJFCw7++pTnnp{}qanw x'x?y'+&Fd70scp}n>nt݂!;}FBFE}}~}9 }JFNE~}G>r22/ÛWW}}}too3v;wܯxC0BE>Wyy>';}؜w߸q]\ ?} ? " h ??'p{[p7s8 -%ۛV.\ Nqw;[9UDƮXӎaB\2ssU/pwgq`s-A(%v7e[{w`ݍ;nMxK;vnۤwsȜnyOproᜬx7;;o~popJJN;{V&P8;pW;pU+p7p n;n?qw-uh΁Gg:7Ww wwj7蜖n}߾r9)))7;sܡdddcᆴq00n}}_ cc#_}խ\$L#}9g%%%$svN}> }w߾pN}=+{q' {5'_y npz8+pzwuG;sn;ìnupnsI_jnf;nۻ݇f;nۏݷwVW&o x[tna/r7}Znv7WFt;x[rnNzsoNwO''}ᜤZZVYޜwݏ;q8{v3zZZZ[qKKKMņq1O\*C=pGpFwnp{=[{Ź7+uupOqW:Ov=͹I'[}}ܷo^III?=-߾;}￾>ܯ}ws3{}[wu_~snpONy>O'>ON5y}IrR2roHHo bb"g))))JJJ 㣣޿llll q/Xg ccc`^83z ccc`^  "h ??MP{αpVw {{A76wvκ+X{u®pnh6pA444< øX[}ʴϼ%7uux[a\]3|y7co7 t]Cwx;#m] %s﻽N{oOg8sv?8vݿ𗄼/}o fpm]np{]Ż+pJIx;\*ppqO}w}4votn[_w}wo}r}w}]\;}}uw]}߾p7uxmxCy?y< [+'S?s'h3|y>wd<+yo'}}Mvxw} ޾ 1q  h & !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbg x,pP,~@(Kd4 Ad4< AB^K %d !_( 2Y/%Y ԡz1AHeqC_\ r,l x,ǧѨ%d AB^K & ]dÎ^A>w_#_B^K %d !`S+T(fWZ! -`/%X 2Y1m x,?/%x ja׈P *^k8P,QzzEHGvo<τr߃81A &<,4WC8G^|3|}2/^ EOzر%>EP3. OA>L~cG? hfC?蚫ߡ?g 88(HH@@Np0/|LLT\ "?(8?},sˁ6\&(g~2p|tLf.2 =׀ wz ߢrDd$ާ!!%%t!)/7@V\d)  4@֪{|$ޣrz~|3?qsz%r?VYҾWc'!C||W%<_]<ՠ ?'jXQ8u` O)A/_"s? 8hhhxBC@NpppPp/|DLDObb'8N&&67qQ{|||$'!!!!8 HHHFJBKrRRR_sz䤥%?ާ%'))9 I?NJJRSrRR_sz$$%%%%9)))/IINFFFCrSzld|wc#ާ8EN.**+q1Q0/S{4:B?qSz\\lgާ8DN"&p|$$$'bbbg8EFN.627qѱ||||$$$?'!!!!9 HBBFGrR2_szާ%%%%9IIIOJJJNRRRSr|e%e?))))9))9?IINJJJKrRRR_Sz$$$?ާ!!8N66:;qqqqS{\T\Wbb'8hBBNq1ӊ~d\d_#c#'8NBBBCr|dd'%%%%9)))/IIINJNJOrRRR_Sz%%%?ާ)))(9Y98oHNNRWrz e%%?ާ%%%%9)))/HNJBJCr2?Sz|||ާ8ȸȿEEN**.+qQ1Q?s|DB>?r|䤤'%%%%9)))/IINJNJOr|奥ާ---,9iyygJɿ~NQ gI=@~FMqrRϏӖ~奥ާ+)++9I9IOINJJJKrR2R?Szާ8EEN.../qqQs|DDDG"b?' 8N2627q||||{$$'!#!!9 IHNJJFKrRRR_|%$?'))))9IYI_KJNZZZ[rS{奥'/-1,9iiigKKKNZZVWrҲҿSzާ%%%%9))HNFBBCqSzlllo"ާ8ȨEEE?N&""'q|\\\_ߧ8ǿN>B>Ar|$$$?'%%#%9)))/IIINJJJKrRR|%%%?'----9iiioKKKNZZZ[r|奥'++-+9iyi_JNRRRSrR_Szdddާ!!!8N6667oǡك/:K<*&"'qQ1Q/s|D>>?qs{!>.󉉉~LDLG""`'  8hhhoĿN*2*3qﳎ|t|ts#'! 8HNFJJKr|d'#'"9IY)'JNVZJOr|奟ާ-///9iIi_IHVVB?rﳕzާ''#9)HBBH?ﳐ||t|wc8XoN&&&#p·\׾?pАӉTLC᠏5)CDW q|:" h(C4r~gO2 GO=Pl>S42{\q0'yy] $t奥e_'---$KGa Ͽ z[O,]9YIIGdd?O;ZH='c5sߞewq{z>fgӿz%R q| &@/@<N&.")XwwW{:<?Y,3n<蘜;2?IFDP? z8fp}|lt^#d`vWA3~dzr0e +C7^8)'9P)Gt<pӑ{z[_o{ ߡ'--+,#_!5:>K%%%g+%+,(=2us7iJHJ?>#8-a˿4(oOdl#do<;g˪܎&Cƿ3q1}r\3%3\\\_r0@ f 3~ltl{g!f :0?Dո.5 rROe؃(P }Z@,b%"9))9'FGUZ#\@ǾARVRQr7k /xa3<0EFVFQrҲo2=qHCadjV jIIHȿ1+ -@ros|{dcުGOUh` |%%e_o H5O=0[d%$/'v9mm%JBNRWpei3J9V @'R{KM^~=9HI.JBٲ)G~69Ǫ@%'>? GκS@'׺=IH>NF>F?l·}?Oxp O¾* .9F=+5OWgDEF9A"+qQqӌ|dLds!Ÿ~Ll#ߧ!8HN>:BGq2/ﳎ|$$$_'##%#9)/JJNRRVSrrRs{%%%?ާ)))8IOINRRRSr22szd$!%!9 HN>:>?qzd\d_"'8&&#p/}LTTWߧ8GG>N>>>9qӐ|$$$'!##!9 IHI?NJJJIrRRR_|䤿''''9IIIOJJJNRRRSrs{%%%?))))999IOIIINJJJKrrR2_Sz#?ާ!8GFN2.2/qqQs|TTTW" ^8xxxC@N&&&'qqqqﳋ|dldocc'8NBBFGr222?|$$$?ާ!%!%9)))/IIINJJJKrRRR_Sz%$?ާ))))9I9I/INNRJOrrSz䤤ާ#%#%9)))/HNBBFCr22Sztt|ccc?ާ8EEN*&*#q110/S{<<44 ᡡ^8N2627qѱ|||lw'!!9 HGN>BBCrSz䤤ާ%%%%9)))/IINJNJKrrrR_Sz䤿ާ''%%9I9)/INNNJKr22R_Szd$$ާ!!#9GGN>::7qqSz\TTO⢢ާ8DCNp/S~T\LO'8GFN:::;q||||#'!!9 HHHNFFFGrRRSz䤤ާ%%%%9)))/IIINNJJKrRrRSzdާ##%%99)/IHNJBJGr1Sztt|t c^8N***&qQQQ0/SzLDDD !!^ 8hhXXDCN&&&'qQqQ|\l\occc'8GGGN>>>?qz|$#?ާ!#!!9 HHHNJJJKrRRR_Sz䤤ާ%%%%9)))IHHNJJJKrRRR_Szd$ާ###!9N>:>:q/Sz\\\\ ^8D@N"""q/S|44,, ᡡߧ8N.6./qq|ltdo㣣c'8>>B?qSz$$$?ާ%%##9))HHHNBJBCrRRSz$$$?ާ%%!!9)) IHHNBBBCr22Sz|||^8F@N....qqqqP/SzLDDD """ ^8hhhhCCB= pӈ~DLDO'8ȨEN.223q|t|tw'8GGNBB>?r/Szd$$?ާ!!!!9 HHGNBB>Br2/Sz $$$ ^!#!!9GN>>>:q/Szllld ## ^8DDN""&&q/S|4444 ᡡa _'hfP-ix-C4 kH( lf%` Z@^ `11-`/Z 0[lx-P-|\c`( l obA@^ `` 3 0[/[P-mx-6 kh( lf65` [@^ `1/Z 0[nx-P-|l{p( l ocہ@^ ``  0[/[P-px-Ǹ k( lBA` [@^ `2/Z 0[ypx-P-{|{( l oc@^ `` `^ 0[/[P-px-Ǹ k( l>=` \@^ `1/Z 0[/yox-P-{tkp( l l cc[@^ `` `^ 0[/[P-nx-7 kh( l.-` [@^ `1qm/Z 0[/ylx-P-{TS`( l l bb@^ `` `^ 0[@/[xfP-ix-B k@( lf` Z@^ `0l/Z 0[/ex-0B/X `_& !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb1|  "$h > >=]qw{n8\6k; v -@v7 wcv7;]7 vܮ۶ܮw6 v7q x+rsw};]_~v7y>}=vw6Mwr {-›?9)))/JJJKx,3~}pvpv7oۃۻn۷wWW'tN'Sppt8>ݷmҹeq 7mw܊:[[ۋvC8+ȫՆ ￾w;Oޝ]oNܵZ~~9/}ow-pOp_}}{zyޝow-pT_}}~ݧ}[ N _q{}~I}'tti~sO/ b "(h >p;;pqwq>+n+\+pWü%wcxy x݇v}.;ߺ].^}7yyv7xݷo7}}+r q}[}}9iiy_x›? -n=5pwzq;{'UĜ3579 \dwWWW]]_}ﺿ}{}ww}ﺆqqoϏn{w}}g'%'%w[P2.2-ߍ{}~III>t}}ﺺ:{+p[z=^O~sd>OОO+{;Wޜ弞O'܉''}m  ",h >ppwp;W[r\[\Mnpo;vtٻ7W]wAoonw}WMx.Moܯ7߾t7;+܍s;}sni̝mɻ x=/ }}zz￾}݂c8a's𗂿x}}cp+8xh pm=)[wnwqwnx\Wzpk U/3sNwDq_:rNn7(}}wo}߽<߾~9_}w}}9} yOޟ}}u\ my9+D}wwxgg[O7}g  y^O*}v;NӍ^Nx~rw2~  +"0h >pwqwws\Wq\6W nE 𗁗f%Ѹ/x>﻾庺7;û};O79{w oWy-#xwrܯ7s݅o^W>rúNwo'2yOO7}RRVWVRVMyݿWp_q111⻼3z3pn8?p{{p-n'q\Mnp]]wWW}ٺ܃;m}w[Pd￾ﻺ[:}Dwiܯ=W{ﺿ2~tޟ}}￾߾w;w}v}^ O'^O:tty>qn?u~v߸=<ޝ[? $dޮw}  m"8h ?? ]p=s}zu+q\W *W p\"Ϻg[w¿]/ } {}x[so }wo7 7;' r_}77A7tnü%yf}}=zo;x?x;'{}?7 !® {[q ݛpsqf wp›7 pDNiHG:no]7=:y:oݷ_}wk{߻ᜌtoo}NRNQ߾} ss߾}o7}s_￾zv}~W}pm^<}\iNb}_y9_y> 䤤?  "sru; s7}&[yw]3|gx'g))))ᜌ3zCCCg//-*J?r22R_P>>>?xw߹\g#### ddd޾E}FFFGpa e 奥e9iYi_{p->{gynݿn#xMn7g!! pcpaq8on᜜=>uuwIo7+ ]]]_} _}ȟW_tN[7w;}'3~}+}}}M_qns%߽=q0O'}W}?~y<}W<47Ͼ'4̻wk'}Sܯ'}pKg%%%%rҿ^  "Dh ?np=6yiMsBh_4/V¬7 i+1hߜ5\+p\#}ﺾ=8{owA7nE}/;8.fw=žHGgܿ]\ù܍}ry-\]ncxwvnWx#x'q] wtNO￾r}~CCC9IIIOJJJCCCx+3z c޿^Z^YN`qg!#߽? ccc޿bbbcsAļĿ3{ dddޮ8GKKxg////RRROIIIrrRR_pFFFG*}cpq9  qn;x[ x;;-ww n vNwd۴<+Dtrk+F >3}v}w?~{}}nN{}}߾y}oDE?>aLLD;}~_~CC87Wc}}y?s{u}r bޟ}N&xknimI?}~￾ܯ|3|=  "Hh ?'p]t+w=`=xw 777wv>oxCpc0#p`trwitoWuq^oӛqI7ې:wWwg!כ};sܯido7{t'wpp w}}ܯ};}~W߾߾p}}˺7廾 8g }f}t2}n7WrM8ȸn8xx3~O448ȸo xK!}#%/77%/m3~{ܮW}þ}{wuy_}rv7y?s\\W߾rOv?ݛ?W n nWxoq›\w-n4&૸=%>wsn=sI&}u}7ϏuПw}ﻺ\ w_}o7}s+O\ﻺ&**)}}++}};y;OqﯼWwDw3|wtu[_y>o g 2 6Ph  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbe22Y/%YF`C/%X 2Y/h x,ƐP,}@(Kd5 Ad/tt  A>(Ȅh"__ЮE' vT[ ]- 0g}uS_,p$ _6 ]C(B@2"}p:L$do*^.*`bW`%3~~C_S ;XWroO8"p|$,$ ``_' 8hxxxDDDN&&&*q1QQP/s{S@DWp\8\S(@2&:&@.h7p̜3w o $Z@gc.% :\!!PP4/G\#߼Cr(4/W<C }3!!y'HA:`(vBp/,ȇ@ qA=\Q@E h?ߢ|d*I2{A(q/h` }{e@جVB/V)`ȿF^ wp\ ]8-wO(T`yޞ8xxxCNp/|44<< "! _8EN.*.3M_|uÏ! _VP W/tT 1|4RWnh_p |& C".;.yy@4ʜ'΀.@>E?9H(@ qS .¾E{Pr Y,ȧ@nPn9@, 1}}e r yhqאw /Px U\ȵ0 ~t\g*)\6*^cn% ߪŴ}tCS .߹48xNp/|<<:-ŕd9d /A"$xp/eVwĿ""&%p|4447aaa' 8DEN*2./qqqq|lllo cB|p3>5h?v28}Th?߬_C5n3~4|䣤?0:' c$h?[hgd[|IIIFBROc\c܀%B%~A?R}"\3z@ȭ _8))?]'!f3~ z3z c<B{~KO*n(7Fc=f\t9n 8mL&@%(WD*&*/q11?|D4D7ᡡ' 8x*.*/qq|\ddo㣣'8ȘFHG:BBGrRR2?s{#%%9IIIRRRSrRSze%ާ))+)9Y9Y_IJVRVSrrSzdާ%!%%9 )HGN>B>CqS{\\tg8N"&pа|LDDGbbߧ8ȸȿN6:6;qѱ|$$$?'#%##9)/IIINJJJKrRRR_{%%%?ާ)+)+9IIIOJK?VZZ[rSzeާ))+)9YIIOJJJNRRRSrrRr_Szd$d?ާ#!!!8N66:7qSzdd\_'8xxxBN&&&'q1Q1?ӊ|d\d_b'8HHHNBBBCr|䤤'%%%%9)9)?IIJNJOrS{eާ----9iiigKJVZVUrSz%%%?ާ))))9IIIOHHNJFJGrR2R?Sz$$$?ާ8EEN.*.+qQ1Q?|<?qѱѿs|l\l_'8xxxCCCN&&&'qQQqӋ|\d\occ'8HHHNBBBGr22R_|''''%9)99?IJNRVRWr{奥ާ-)-%8))gJJ??K-/#yrҿ{%%%?'+))99)9/IIINJJJKr22RSz#ާ8FEN.../qQ1q_|DB>Cr|䤿'%%#%9)))/IINNRJOrrr|'----9iiioKKK?NZZZYrz奥++-+9iii_JNRRRSrRSzddާ!!!9 HGGN:6"/qSzl\l_⢢8CCCN&&&'qqqqӍ}ttlsg! 9ǿ>NFFFErRRROϓ~|''''&9I9I7JJJ?VRVSrs|奥ާ----9iiLLLNb^b_rs{e%%?ާ+')'9I)9/IIINJJJKrR2R?S{]/GC?EGN6667qs|LLLG' 8EN2625qѱﳎ|||{$g!"8IH?>NJJJIrRRR_{%%%')))*9IIIOVVVWrҿzާ-/-/9yiyJVVVWrҿSz$$ާ%%%%9))NFFFGqS{3󌍎}d\lgԑPo{NM?#'],#Gk<$ ߧ+-)997rڏ),C)KJNZZ^WYyhSﳒz}l{ãd]OC}%e$%!y?ߤwtC"ODukD{!?NJF";rrGûGf.%,\T;wjYz qs>_\yHxWjr`G#g0v? 42}y?BN.6&5m΍}sGM$0 ~ 9  C$/mS΃7:rOa qΒ~}d\?Bտ}X #|?(P~~Sܧ ]! Wgw~GFZ-ӗ{ӜscOI$iG.~EC7GJ?vnB 4#n\p? Mtbw IG_p2p nrg@ 8Ș÷GGa`dYi¿@%QsVP|3DLLK$Zx o|o_ Ӏ) 3R6O|d$$'!!ـ(ji"Ag 9 '8F!z+|KXȿBNJJMr/ff :0gros|dli"Ag,9iiigJKʿNZZRQrҟ2A#ՈW@.= r~  :Pg@ {#!%'$9 )Eh)o? 7؉ǿ=92x ߹-̀8BF J Ӏ)0+Ŀ&&&%8wDA,3cMN&&&'0sOD76󌇍~|@QfX=r2ϳ~|@{M5Q4W=3~|r 9GFjrM" 99997HH?=մ(@*rOSzk 5WY3ze!8؃Ώ󅃂|;sd~j\ǾBRVRUJ%SΨqOVArrR2oszZF/W?{fo󏏏~{{etia@ \d\c &ZX&[5`_DE?>N""#F W)DG oU|l (3Y2sg9GD>p :8uw ]9HII?=2:0?BUZ.ArRrrOϓ{,Gf(%\.I 9iYigHv ?(=_C9gYΔv p%4_4PUR󓕖|d;r~zLg%OЇV\cߡ9)) 'II?^-t@+o7yH?=^92w :,`8:.=,2M G k?"*"#q1úq1Q1?tK`dxF~ھ)FEƿi޼3ȯqd#A~5ώN ZcϜWAx3~|S#w1FOZ.Adߡ9I))'D>j9.Ih JK+!%1Qم`& g y'0N;i=99y<9Hx/6GyQ4q!ߡ'+%'\982;C7WDNRJ:Er2R1!aӒf쁛Ldb#_A/_W;\t*Hh22QؼrC7YAN&""#pа PZ`h?aa(_h)F??Gր%\9SYdo?}oq/hg 85_:1&p ~~~|#(}5L#?'))($h,`~I?Ѹ$_+%)(G Ci{~5>FE')DI?=H?Ѥ_H?@:JFIrQr1Llw@%dscߟ#o?F m)DF@N2223&*'&Q?󅈉~L:>;qѱѿS{ddd_ާ8DN"0N@"#q11?ӊ|d\d[#c#'8HHHNBBBGr/|$d'#%!$9)))/IIINJJOrRrR_sz%%%?ާ))))9IIIOJJJRVRWrs{䤤ާ%%%%99)/HHFBFCqzltlw㣣cާ8ȨEN.**+qQ1Q0/s{;s_y=[& q1?ӊ}TdT_""'8FN>>>?r|$$$?!!!!9  HINJJJKrRRR_Sz䤿ާ))')9IIIOINNNRKrrrRSz䤿ާ%%%%9)/HHNFBFGr2Sz|t|wcc#?ާ8EN.*.+qQ1Q_SzDDDD !_g 8xDDN*.&'qqqQ|lt\o㣣c'8HGNBB>?q|d$ާ%%%%9)))/IIINJNJKrRrR_Szާ''%%999)/IINNNJKrrrR_Szd?ާ#!!#9 GN>::;qѿSz\Td_⢢ާ8DDN&""p/s|<<>>>q/Szll\\ ^8ED@N*"*"q/Sz4444 ᡡa`_' 8xxDC&*"'q1q1_|\ddgc##?'8GN>>>;q{|$$$?ާ!!!!8HHHNFBBCrSz$$$?ާ!#!!9 HHHNBBBCrSy|ާ8N..22qqqqp/Sz\TTL b" ^8xxxCCC@N pO~<<4?!'8xN.2./qqQ_|tldocc'8G>B:;rsz|| $#ާ#!9 HHHNBBB>r/Sz $$$ ^!!9NB>>:q/Sztldl cc# ^8DN*"&&q1/Sz4<<4 ᡡ^ 88888B4 k@( l` ZA@^ `0 /Z 0[﹇ix-֠P-|=` [@^ `1/Z 0[/ynx-P-{lkp( l l c[@^ `` `^ 0[/[P-nx-ŷ kp( l.1` [A@^ `1qm/Z 0[/ykx-ְP-{LKX( l l b"@^ `` `^ 0[`/[fP-jx-5 kH( l` Z l aa@^ `llx-A2zŰZEl ? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@C2  W"Th ?{@͎ KI:vNq5snΨvGpep{|2l \Ւ T ;f\P[ N qY '87qq/N;an sᛄ AWMqv݃\}ی-!uw}}ߺܯ06666K>7_}{ wlM]yrna.Aoyxw~߻87-Ovwv] }?ᜌP)mq ^>ݾn{{tmt7~M0 n Npp.wn;N7y;ny:]\;qxNtgwWȝwzr_r' .2&)w߾wwW~}[vTTTSg߻ﻻ}}'zvO};>~y<}ItOIuO[yIؿZw  x"Xh ?2_q1}-'w9F\ #$ ) zTjWx.j8 |<O8"LBIH08)uS)U wW*087U-.WwnI;an  \@ w#tUr3|?'iӴ}}wvx& }}   y7}ywn}O'sDr~9_};yWy<}s}i}~/JJJKRRRSxg))))9999Ox+xx[}﹛pp￸o7kv7oso{y{xnCܯ!C{7ø:pø;]n7ww$kܕ7"pܮWWvݻޝwgs'և?N}￾:60u}}p{o~{}+x '}&rRRR_ j' ! "\h ?`>@%8E.:f{pNEW Spm0G=O,ׇ,s(Vc@!T?ߘS\\rq CyGWw+#u}#q7 U[\cp8 0;U*F\w܍q\ ە_}tO{t}7Ot'\_sMv }syx.xw#s^ }}t7x*ܯ;'WDw;7w~~OOo`v}3z?}}%yo xxo x)oüx;iSpTw7;H$&mçƜfޟurܓkNWon$ =}v7]so߽:܋v5`v}~߻nuu~W}~H?>>wi}}op{'r7ݽW{ݧ]};p"*"'w{sO'wz}@ZUC +L!, : "`h ?>[pNqG Zl5k: [lj,CVcOBA6 F ZXj,t$ '7N1G\kAcX!\ =~pR+<}7H;_xKWWw7AW*7t#v X&n%*w@n sWao7Oo}wɹW}}ܛ71- ݧ};7+yx۰O'sb}9{w+w}̞OxNKK߽wt7"x;ü{{ %^x=x}߸U†wwۄv=÷Wq\M7 mS37}tpQppϫOO^}[}ڽ?uv}}J;ݻwu9w+_}]+ W\\\cs]}W[W}W`1>Nnۻ_}}}}"o'_ E  "dh ?FCVc Z厊}p!\  Z压{FCVc Z厍yx!\rFA>n X+k\  pPpp/ppww pUqcU#t ݂W T3{q U#pPnw`**np`U@ @p00pp/o};NwWۙ97Mtݧ݉888HH}u}WWw}}3~܍7`g}or}}+}}v ݷ߹\symȟyy>F9JFJK8 c?᜔g D8g))))ے8_g ###߿g))))߼/V ߞRRRS\ at|T̼3^?2o7o>Cx pcpQ҃7WmnpqI>cp{n6-͸oonIq WN}}ѾpP[;n9?{a'qQqC??. Նj;_tﻺ}x\ Նwuu}dt'{w}}}ka #"}v}Н9I9)wy>lop﹛y>nw{{wW}tɭrRRR_8Hxxg3~ ` "hh ?;®c;g .® BLB%qn Gt݁66B6sn B@ 0#pKpP p/÷nW_s'ww!t7ѺnW r7MWDi}}ux}O}ȟ}}}ݧ~ovOW}ywwv7+﹛wܯ_}I}}OrA?s^{'''WO/3~gg%%%'@gw0VVNYgrnW+3~Aok3~C}8_0_- 2.23~x;^x;{0KU@\\LST p^,-Îcq݅}rp "CpppAw ; \ pAW>=n]'6qo}I;tttcp2..1ߏ;v};ӫ[gy>@?>?}ݧo>,pH7{;;->}<_}}[yݻǼnwӹ^o}xCwRw>KRRRS,k O "lh ?:P«nL\Td g DC8Ȩo62*.>F Eoaldlw3yx 㣣#n 6.+Ge=p`Fk .0UÂn\ xosnNBBMܮ@D.Wwr c#cޟ};~y<}}ܿ};;}~};?}M໰Uu#s}.7;M'Wy>s_W;nۑ>:swM43~} xg/}8?q  a⟟Xg/='?x&Cw+x,!n L3~}p H[n0 }@rqa\!3an 7 ;WOݧDېg;mvNr}(?swDW>~7?%Aᜄ~;}ܮhoJPds7nw}p{Cu}-}︘g}srwOy߻q_r73}߸Wc8Z g @"ph ?Tª02qRGlq11Q_>6262;qѱѰOXg8'%@L3{ ⢢^8 ⢢⠞`_q7@onWo/܁ x&Bn7}226-# "_?Wb}ܯy97ܯ7tOo;NP"&*)dTl_Do***-\Ld_7~}̞o{yW}rtttc-oy>}}OW߾y>}_}ݧ}'? w$wAM }ZZZ[=p\&:9 뺠Nᜌo(7 bbpn0 ,;wan0qv sx[;r`& +ø@[۱7 pP,3~}ϺNۍnݷwjrn#o7rm8}a_x}~ܷO5wkts}}߹zr}߾￾uΎ}nWw6ﱺn?}w#}sU2sss}~xy<}O'ܯ7rWl1>r R b"th ?uv?@_X*ۿ3y3^}a|||5oat|| x& E.;3+'}܉W;;};O}+_3y>}O_}}ywO<BGx#};y?}oWmy>9iYiWt#zxx'};ysxKᜄsr-}]}/6 nw3y ܯ  py9̜}xg0w]  "xh ?whj,sXVc 7 n? Fx8g ###?]Mqѱxgwa| gݷׁׂ0..&3F7 8hxxpp> 70M* .0>s'`t7mN$0!f444/g DCq11?}2}}wtW q?}ܯ}ܯv7<[+q1sO'"xgw݁wۂN}}pапg NW='+x+x#s>?8hXxo3|p cc}~̜}{WWBBB|3|￿4<,7݃rxg/+/)ts }#g++++#‚0?4447n4=x{~dddcgn qя=vx;=n0pEnM AtlT{nA.&p\-ݥ{mp}[t#wus݂ow&n;WNZw}Iw}{s ^<}s~;};￾}~F쇘n]]o79)9'ۤ_ۚH3pG "^O'}ooo'+wy=>w}III}  1 "|h ?CC@p8ȸh ۅ_g  ^>>>>x' ⢢ޞ HpGppM/n;BFF  vo ppp?p`>o;7ܯrrn}ﱽ̞Ow߼O}}}vrW}}}}v'y}y}srrWy9ۃ8x;+߾ eee_}}8ח~*8x;7W=+qѱnW?}a4$#g }y< }o\Wv_=y} Ĝ7< 3 h  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cbd22Y^K ^K dQd3x,(83x,pC/%Y FC/%X 2Y/h x,ƐP,|@(Kd4 Ad<< AB^K %d ! _h 2Y`/%YFP,k x,Ș5 AcX(KdD@Q%d B^K **/%X 2Yqtl x,P,{`(Kd6 Ad\d B^K %d #"_( 2Y/%YFP,n x,7 Ach(KdF=Q%d B^K 62/%X 2Y/Tm x,P,zp(Kd7 AddtB^K %d #c ^ 2Y/%YFP,m x,6 Acp(KdFQ%d AB^K ..ꍠ/%X 2Yqp/Tl x,P,{`(Kd6 Ad\\ B^K %d " 2Y`/%YFP,j x,Ȩ5 AcP(KdD@Ѩ%d  AdD< A AcH(KdC@Qd4 Ad4<  r,h2 ^d4zY FpC/\ A r,}(!Y^dA@Q,PC/\ `_heAc !Y/d2 ,N ppp/|,444 ᡡ'8DEEN""&&qQqqp/szT\\g ^=8ظG:::>q/Sztttw㣣^g8GGG=N:6>:q/szlddd ԱqЇ^ t 8ȨD""&&q10/|4447a' 8H(H?AN  o/}$$ aa_'  8hxhxC&&&+q111_}LTT\ "8F@ٕSNV * d 㣣8@:>BCq/Sz|||| ^8GGN66::񌊝 $ d^Ch _d|l bb^8EEN""&&p/{<4D?a!' 88(HHAAA@N pppPP/ӄ~,444 _' 8xDN&**.qqqq}T\?|sA||l$$ ^!!zBF"Cr1Yx#!8c# @f>= @NB>.>q[\I /ˊ@$^چ|kH4n8ظEC@N..&/qQYo@&&"&q11S{<G?;:)j( 4 iG_y`4fAH+Ph놑ՆP/[ sOCGd`(>2,@-2x+22dF ![^)2m_Xd:` /y g8CDÿNpп|aaߧ  8DDDN"&&+qQqq|\\dgab?* ((4S1Hº>U5\Ax곃@ wSPKĿz,ȯ8A ~ӊSƿߪG@U0hETy04>FNJ:JA@%@ )UPh#|~}JK4@2'?i埀Y{G@h?WEXK1Vn[C˟ $M,X=K_HS# GCς}9\g[*9޸C1pOx C8&&*+q1s|4447 ?'8hhX_""#q1Q1|\\d_##?'8؈؏@& (T{P5ΐwNA9c>G@U hD} 4>FB.>/ @-Uh|}4@h#?ߧ%!'%ThE|4q=?>UAfPh^ b4󓎓{4`\ph$o $4O7;X>B)׏{ߑCd:'ʹ\k^9[+83q83{ 8ED""pап|aaaߧ8EEN.2.3qqqq|lllobc?4 (2H>D˴߷h _6o~`4uTo?>G4A ~z '"#NScꯃ@$YAW2@+Th#|UU_>H@!y@h$@*㋸ ȂPR '('92_K*q j(ƌ|R/Po{ddd_ާ8xBNpppp~DLE8|3{ ~|i ~htH4߬7qzd\d_ާ8xxxoBNpӈ~TTT_ߧ8FN666;q|*?ß|䤤'%#%%A)/BLY)?JJRVRSrrQFzY#ҿSz$e_ާ'+--9iiioHJJNRRSrrSzlldާ%%#8 GHN>:>?qѱѿS{d\d_b'8BBNpӈ~L\T_ߧ8FN:>:?r?|䤤'%'%'9999OINNNJOrSz奥ާ++-+9YYioJZZVWrүsz奥ާ-+--9I9Y_INJJJKrRRR_Szd$d?ާ!8F622/qqQq|LDTG!!' 8xN***/qQqQӋ|ltlwc'9  HNJJJKrRRR_|䤥?'%'))9IIIWJJ?NRVRUrҲS{_'+--,9iYioKKNVVZ[rSz?ާ%%)%9)))IINFFJGr22?Sztttwc8ȸEN&"&'q|D6?q|$$$?'%%%%9)))/IIINJJJKrRrR|e%%?'))-,9iiYgKKKVR^[|AKJʿ>NZZVYrSze%%?ާ)))'99II/IIINJJJGrR2R_Sz|#ާ8EEN2..3qqQ?|D<4aK?ZZZ[rҿSz%?ާ%%'%9))IIINFFJGqSz|||wc8EFE?N&"*#p|LLDC߿8FǿN>>:=q||䤤d_'%%#$9)))/IINJRNMr|奥e'----9yiioKK^EN~g+˻=my6E޺M rϓ~{奥eާ))))9I99OINJFJKrR22?Sz||$?ާ86.6/qQqO|DNJJJKrRR|%%%'))))9iiioKKKZZZ[rHF (ABy3~{奥ާ+-++9IIIOIJINJJNKrRRR_Szd$$?ާ!!!!9FF:267qqs{TLLOaa' 8xxDĿ*..-qﳍ}ttls$$g#!# 9))'III?>NFJFIrrRr_|%%$')+)*9YIYOVZV[rs~~kI$ |"$^+1.9iyioZZZ[rSz䤤ާ%%%%9))HHHB>FCrsztlloccc8ȯNpsDC@h ߿䉊tll\ccc'8Gȿ>NFBFArRRROϓ~|$?))))9IIIGJJJNVRRSrsz奥-/-,9iyywKK>N^^^]rSz%e%?ާ))))9IIIOIJINJFJGr222?s|B="g8FF22.3qqq|LDLG"""?'8h4FϮ<:\[ſAN.6.5qӏ~}|||{$$$### 9)'IIINFJJKrRR2O|%$'++))9IIIOJJJNZVV[rs{'//--9yiioJKNVZZYr|䤤'%%%%9))ǺR0g:Qǐa8FG:2/qs|\T\Ob"b_'8hhhoDDDnQ W-W.-t8hGǿ>>>>=rӐ~}d'#'#!9)))/HINJNFBrrr={rrrRﳔ|̬qJƞ| '1/1$,lQ%eHN^VVWr|drQ3%)zdc$?'## 9 (GC>>>?ЎfeW+t$!ߥ=$TW'DDD" 0G?N"""#.CTj jp)Sg᏾t uj=ss$_^sH4o@ `C( ~㟣c_,`6J6M2W13m ,,21ݞQȰ//Yc_ϿJ}o4oIK}ҏJŻ4WMJInlϲkT+9YIi? |kt2#I[㝼1!Z?BJBIjg:4oA` |l|[v C/ fЖߡ$m?&B,{LsyfN""*#pQ`2L</W"&&#_B ޠq/г~32~R4S~z/Qj`>> [$dd$_ߧ###AKh-"H(?>^A)i3~t䤤))$˞@3|%%F(~yUh$UA|~td_?!-,9iYYWʾR>0gӐNRRNWƹ$OJg\$䤤ctsN@ T ~td(sD#cKç@  $ q  8^#: :­ˊ? b!b[۠gG$T*`4S@ ~Tl\kacaߟ"J>"R43~y/ $Z4`>F=( 9 Huh-x՚["y tÝSOa4DrrrrOϓ|!l!5|3--)*^API0hu@h$ ߠ9yyYgH>ɨ"Cy?Dz 4g0ӈNNVRS90ܘ_Z ~޺&Z&[<##_H>_\ðfxa3}||t{rxX.%qqqϓ{÷~neSC$}ϯ< q111׆Mt Ĩ\S4#c#%sEph@)LF?=W1tр.5iHIH!!!H"[Eb%7ȺT?>} 1ӓ~zfA 87ɿ?NRVQ輂4@??4T9ו)q;C?iIV2x 4g&-r~1ɀ Ǭ\{$䤤%#)0@),'yA2~C3A,1 O)\Jp=qqqQoϓz!ϋ|筹TTxMb{ !!ag\NnQN>n\\L[~"Z4@XgǺ-8?>B%ì*&j` 9pǜ!gz4w~{BD2ǿ>lѹ[j~TԲQRO}=zj~r}~ᜈO[D >h?] _C(ހaᓄ ׏EzA?m3k,$?rd K h?_9 97AGH;@3_Gƿ-΃l U|֠ ~??f"ߟ8~瞘v Oշ~|+ ᡡg8HHCEE?N.**-qqqßTt|{#ߟc?!H3䤟g!!"a'NF2NK82_{|l㣤'%''9YIY?#J~FROrrQ{Is|%$ާ#!')89/IHIJ:JGqRSztdw⢣8FF2..+qqQq?rDD@ j{3g 8hxhoDD&&&'qQqQoӌ~dldkc#c_ߧ8HHH?>NBB>=r222OӒ|䤤%%%%9)))/IIJJJKrRrrS{%%%?'))))9IIIOJJJNRRRSrsz䤤ާ%%#%9))HNFBFCqSzttloc#c?8ȸȿſE8_⦆'d $MP/|<<44 "?ߧ8N.2.7q|t||w'!!!#9 HHHNBJBKrRrRsz䤤ާ%%%%999)/INNNNSrrSzާ''%'999)/INJJJKr2R2_Sz$$d?ާ!8FN6663qSz\T\Wbޢ? 8xCCpӈ~TTLOߧ8ظGFN>:>;q||$$?'!#!!9 HNJJBGrRRR_Sz䤿ާ''%%999)/NNNJKrrrR_Sz䤿ާ##%%9))/NFBBGr1Sztttocc#?ާ8ȿEN.*.+qQ1Q?SzLDDD !!^ 8XXxCN&&"'qQQ1?ﳋ|\dTg#b?'8N>>>?r|dd$ާ!#!!9)) IIINJJJKrRRR_Sz䤤ާ%'%'99))/IINFJJKr22R_Szd$dg!!!9GG@N:666q/Sz\\\\ "^8D""""q/|44CrSz䤤$?ާ%%!!9)) IHHNJJBCrRRSz䤤$?ާ%%##9))HNFJBCr22Sy||t ^8ȸN....qQqq0/SzTDTD """ _' 8hhhhCBNpӇ}DLDGbb'8FFN6623q|||to'!!9 HGNBB>?qSz||$#?ާ!!!!9 HHHNBFBCrSz|$$$?ާ!9N>:::q/Sz\\dd ⢠^8DD@N&""q/|44,$a`' 8hxhCN&"#q111|\TTW⢢'8FFN6663q||tw㣣8HGGN>::;qSz|| $# ^!!9 HGNBB::q/Sz||tt cc`^8EN..**qqqQP/SzTDLL ""a^8hxxhCCBNppp00/hx-րP-43@( l oaA@^ `` ߳ 0[@/[P-jx-D kP( lf&%` Z@^ `11 `/Z 0[kx-ְP-|\Sh( l oc"A@^ `` 3 0[/[P-nx-7 kh( lf61` [@^ `1/Z 0[ynx-P-{lcp( l l cc@^ ``  0[/[P-nx-G7 k( l:=` \@^ `1/Z 0[/ypx-P-{|sx( l l cc@^ `` `^ 0[/[P-mx-F kh( l65` [@^ `1m/Z 0[/ymx-P-{\Sh( l l b[A@^ `` `^ 0[`/[P-kx-D5 kX( l&!` Z@^ `1@/Z 0[/yjx-֐P-{4+H( lx-3 k0!-|(!-e"0[/d"`-Ɉ@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb3  "h ?8ȸ  \,rnNWx&NL3zq/O; dddgヂ Kpy;n<9\mڻs?}+w~ r~n[:wDxw߾{ =s}~}Һot}+}nnݷ߻nٻtWmtݞ/_BBBE  "h ??pF`@.EMͼ{=ppN`'0w qoBMU}&@8P*W vNm9}~݂wty>ケ q}js>y>W}'3s7Izx&]߾}r:6w;NDy>߾}o}} s;t Opq; oW 7 ۂ=-f7qqxɺN$ C8ݷwdӸ5vnWKv;ޯ}Mv}-\mڻoWou\ܯ}}w}}~>ܮwOv}y>v'5}}o7OoO}}ޟxg߿ y?_  *"h ?>? ,}#8oa||\o7 p`N÷{r'isy9\7y<\9}}w@Qޝy>p}ݧ;`g#%%#wy}- o';ȟ~ޝݷ}}ync/y>O3~~x?r22R?[nﻆqѱ;wmaTdTgw}p8w a,;cxۘw[۠!Crwzr7(7! 7;}ٽ=:Ov'wM彽}i߹C8߾[=o߾ۭr~ᜄ||s㙿}}rHH?<}8v0:661}y>naW}O܉~߿}z  K"h ? c# ^U'`+?66*/FF@qqoO80F>:CMMnӕww@TT9I)I/}}'y%}f}}+_t''vO߿Z^^Y {}73z8C*?(n8M3}.7 gYHfnݷc>nCrM7 oo^o07\\f >iۛ~NNtޟ~;%~sooo~y c"?ߟ~~~}ww;}z}?u}Iw+u_roO ⢢ߞOwy8  m"h ?,w+wDc ᜄIIA<9 >>F>|3y ccc`^ ⢢޿ 8*o ux[w3sU;\@fy`]3P*@v_rۛޟuu}wWv7ws>ov >}r]<}vwtOy9_s\WNmy%u}wy>oy?}xv;mOyEDp$ݿp?Kqѐ/Na7 p=໰w` }K<<"4 tnv?rrn?p8 %-;qMn{{;]7#.*.-ݛýw;۫۶j?+t>}w~;MWiOJ;w?WiݧNwW}}_WV' ccc}o|3zy{N<<47 j "h ?Ag p3|w?ݿrN_$$/w=>%v{];wrA. U4O_}$}3w}N{ ᡡa߿}My; {x[M}}ywM}qѱwﺾp}''}ޟtpgޮGpdu r "h ?8  FEX;FFEvwr22Oq݃ dd$?ޞr ? >7r'{vnivoOnWp<+r}6tr>OܯW-y9y>sy<=㽽:Orү]sqy?8qw4y>NwNwWd^O'[_ o+޿_} w "h ?llll A.@662;GG; =/-w@@xNp mM7s{{}vĜ0W?y?}>zt]\v}IM&ݧo.^o7ztOϺw^ >sO}_}cq_9iiiosͻ+pW+r2ROgwt_pq;7=fw {m };3pnpo}wwWqn{nw;^Ou{߻w}ﺻ~}7[y^w9IY9Wwﻸܷuuyp1Qϟ}s}:'dyCﭹܯDLDOg 3| b"8g}}ܯnWM3z ᡡ0 CBC8xKO}vWy;'2y>FFF ࠠ!,4,7g܉̟}Ȝs.FFFq\3| ᡡ! c#޾C8888O3|᜔g))'& !!!??᜔g''')ݻ8qρltts3| %%%?{$$$'3~|g  ޿p$$$'pv_5¿ۚpWs*FFldlc۫p{p.[q4k5 *.&) po'uy:~rw~W9)W}w}w~}s;]}6}nDO'ݧzsiͻN$r'}᜜ 㣣޷O/W -5"h ?**2744$,47g  BAp00p0/8hhx3|;/>>>>CCxf3}nᜌܛ&"&'3{gEEmÃû ws s x[ú x['A܂57_ cc}}wm}=>wA]'_GGG;Ͼ=ӻ}}1o}t}9:7n܉s;w6}q...);⸿pN]nw{;;%\.d] wkCu}_}v}[;on ngN<3ۢ}}y>OxS==9y<ӐmȜqq'߼~FFFG p:O8 3KVh  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb`5`d  X,ǧ02Y Ac  Ac !,~}!,c22Y/c22Y^K ^K d@Qd3x,((3x,`C/%YF`C/%Xed _h 2Y^K /%X 2Y/i x,ƐP,|H(Kd4 Ad<7AB^K %d !_( 2Y /%Y FP,j x,Ș5 AcP(KdDDQ%d B^K "&/%X 2YQPOtk x,ưP,{X(Kd5 AdTL B^K %d "`^ 2Y`/%YFP,j x,Ș5 AcX(KdDQ%d B^K &'@/%X 2Y/Tj x,ƠP,zP(Kd4 Ad?q/Sztl|t ##^g8N.262qq1p/3yLD\W""bާ8hh@NpПs|$ ```_&7N pPPp|$$47!'8xxN&&*+qqqqs{\\dd ##8ؘ#c_@"#qSy||$$ ^!!8N>>B>q3yttt#c^3!h ^8ظFN.*./q1qSzLLLL b`^ 8XHhoBCC?Npp0p|ࠠߧ8(XH_CBN#p|DLDOb'8FN226:qszkG4zp4l4_|$$?ާ!!#!9 HHNBBBCqSz| ?ާ!eqP4/upph#`'8ظEN.*63q111?S{LDLO! 8XhoBCBN p00p?|  ?ߧ 8hxxDDN&&&#q1_}\\\g"?',d\JTH=8ؿG-x_e.7\o⩠= +EA 'd$ENBFN?1ʼ}.@ $-tԲQL?H?sy|{h! ^ "HH@>"'@.? =7* \~\x2~p/>2 b )q{_1 3?QH}T3+poyTn-Q@Ph\`^kHgƿw!!'`p/GN.2.1qQqq_s{DDDG!'8hXxoNp󆆅<<@V9|4 Uh߯ ܃ڣ\~1/4? C_|lƟ|lƠjP,/5FFF?2.2/qq1Q?s|G@/3Ph'0h _7 >w# ;.x5 oSx4vP ug8FEN.../q11Q?|D:?qdz* ?߸yoYA?X G.7rQR. "#< $`9͌!j_ Qo|'l3Ɖ!J@v  <=i0h>h4Y/b!Š z4_Vrz@ d/GJJSy_9?_Ƈ%h|{^ |b̧s͌S>#A_Z<{tt|c#c?ާ8ȿEN"&p|ᡡ8xEE?N...-qqqﳍ|lltw'!!!9  HG~JKINFFJOrRrR_|YR_s{deޣ?+/9 IygIKNVJZ[rrsz%%%?ާ+%))89/INFFJGrR?s{|8ȸȿN*".'q01|444/?'  8xE?N&.*1qqӋ~~ltls㣤#'!#9 HHNFJJKrRRR_|$'%%))9))IOJJJ?RRVYrsz奥----9iiioKKKNVVZ[rSz䤥$ާ%%%#9))HHNFFFGq|d\lo"'8CNpBZN]YiXsӖ|%eާ+)+)99IIOIINNJNKrR2R_Sz$$d?ާ!8FG6.6/qQ1?|LDDGᡡ' 8?*.*-qӋ~}lltk㣣㟟g9 HHHNJJFIrRRRO|$?')))(9IIIOJJJNZZZ[rS{y 73|['(Pg'-/,9YYigKKJNRNRSrSz䤤ާ#%%%9))HHNFBBCqѿS{d\l_b8BBNq1󉉉T\T[c##ߧ8GǾB>B=qs~|'%%%%9)997JJJ?NRVRUrs{eee----9yiy?͹ϯ|̜{x>:3|{奥+)+)9Y9I?IJJJKrRRR_Sz#?!8F6.6/q|LNJJJIrRRRO|%%%')))*9YIIOVVVWrҿS|ğ} | yG=^^Z]rҿsz%%%?ާ)%)%9)))/HHNFFFGr222?Sz|'8ȸEN.&.'q11|$$$'"""8ſ> ?RBN:::9qϓ~}䤤'#%%$9))'IJ?NRNNOrr|eee'-++-9iiiKZ^Z_ss{奥ާ++-,9IIYGJJRJNKrrRR?S{N>)ܑ?t$$$82.6.qqQQ_s{DDL;ᡡg 8ED*.*1qqqqo׋t'pa3: 3~ltts#ߟ'##! 9'H?>NJJNIrRRROϓ|$'))''99II_JJJNV^V[r|奟ާ//-/9yiioJNVVVQrS{$ާ##%#{dIȞd##!!!N6:>?q|\L\_b"b'8hXhoBBN"""!q1QQo󊌋~\ddkC(DDCyPZʙ$|z=Y)(7?>F9rS/yrRrOϲH91QQdd''A$w= yyEV d<BCFT3|ZNE/y=y%%K)''z$ չ 9>gzEBO3{|*O5= ?㢙C/ޡ'Oᢅ=O=CDBÿN*2*!')Np|DLLCߟ8ȸ6 ϯc ͡ ~\|sU>>2ktaߠ㏈t{MThAOr1OӒ~MTOӑ~{y5~g})HJ>OA~ ?g݃97)JJ>uOUVp DI$#,Hw9Ld奟o&I97s$z$Z4>E OFuOY/s04@ 9$Trx?e9^=2 yчg8- C(poBs?CN&&")C[|<4Gᡢ"?' 8EE.2.-qq׍tl+4gCN:6:9s;<Y44$#ߟ4>0@ 㤣ߧ'#' KViTfag!%$x x[i]A|rMӔ~xG:Ate=h?YN2.>6묪zj<3-[_秷C?&*'qs|LLLO8º2xiߡxWGA9h`9-r2/ϝThAAViH(ȿ@#! "Qy HiIGI>!xc|i@>I@&99II7sÆGAGFW.UӔz; SN S_S"C9oɗe%Y8d%_.IO(ftb 3{\\Űh|d{4>/@GH>K_c9|;QnyӸf} 6w$cߧx2E?#>>C4@~@?+^%᜔Q:4U]V ~|ە8A \>N>RAQ _t!>~V໇(Py0ğ~}q1'ߡo#8}<2K &ࢄ|dr0yLU3?!܍ALWc?zσ5 $&3qѱrGB;j׌Aq11qOӉ{DDDGᡡg8EſN22.1qӍ~tt|{ߟ'!!! 9 IH?>NJJJIrRRR_|䤤'%%%%9II9OIRNNOrs{%%$))')99I9OJIRRROrRRrs{䤤ާ####9 HHHBBB?qs{:25;qo1`Aka8DN""pTTT_ߧ8??N:::9qϓ~|dd'%%%$9))'III?NJJJKrRRR_s{䤤')')99I9OJJJRRRSrsz%%%?%%%%9)))/IIINJJFKrR2R?Sz|#?ާ8FGQbo󌋊~\\\L "b _'8xxhhD&&&'qQQQoӋ~ldlgccߧ8NBBBGr2R2_|#%%%9)))/IIINJJJKrrrR_Szާ'''%999)/INNNJOrrrR_Szddާ%#%%9GN>>>;qSz|||wcccާ8ȸEE.**&q11/s{<4,4 "?8FFEN22*/qqӎ}t||w㣣'!!9 ) HNJFFCr2R_|䤤ާ%%%%9)9)/IINNNJKrrrR_Sz䤿ާ%'%%999)/INFFJKrRR2_Sz#?ާ8GGN::67qѿSz\\\Wާ8C@q󈈉T\L_g8ȨFN::67q||$$?'!!!!9 HNJFBCrRRSz䤤ާ%%%%9)))/IINJNJOrrRR_Szddާ'%#%9)HBBB?r/Szt||t c`^8FEN.*2.qQqq0/S{DDDD _' 8xDD&*&'qQqQӋ|\dTgc#c'8GGGNBB>?q|$$$?ާ!!#!9 IHHNBBBCrRRSz䤤$?ާ%%!!9)) IHHNJJFGrRR2?Szdd$?'!!8G@N>6:6q/Szll\\ "`^8DDCN"pа/|44,/8EEN.2.3q|lldoc'8NBB>?rSz|ާ!!9HGNBBBCrSz$#?ާ!!!!9 HHHNB>>?r3y|ttt 㣣`^8E@N..**qqQQ0/SzLDD< !_'  8hXXHA>Np󈈇~LTDObb'8EEN6623q|l|lw㣿'8GG::::q/Sz|t㣣ާ!9 HGNBBB>r/Sztt 㣠^8F6:22qѱ/Sz\\TT ⢠^8DDCN"p/|$$ ࠠ_3E` Y oaa@^ ``  0[ /[xP-jx-D5 kX( l")` Z@^ `1qm/Z 0[mx-P-|\[h( l obA@^ `` 3 0[/[P-mx-ƶ kh( lf65` [@^ `1/Z 0[﹎ox-P-{lkp( l l cc@^ `` `^ 0[/[P-nx-Ʒ kp( l21` [@^ `1/Z 0[/ypx-P-{|s( l l cc@^ `` `^ 0[/[P-nx-Ʒ kh( l25` [A@^ `1/Z 0[/9nx-P-y\[h( l l bA@^ `` `^ 0[`/[P-kx-ĵ kX( l"!` ZA@^ `0 /Z 0[/yix-րP-{,3@( l l aZA@^ `lf` Y l `A k(!-| ![^lL@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb?3V :x"h ?0? cpc}ӻݎnW7Ɲtv7ixgmu}7Ig#!}cvOt7绾}۲Wb}snӤo7c{n_+{v᜼ *'EIĿv…w' n' ۅ'pq9ĝ خNN'ON8RRpew6==> ==\d1^{n߽r7;w}}~t۫r_}"v_m?}߻^on+W}П<3uy>}'i< xtUv nᜤg)))(JJJ„yE  "h ?qmܮCr;us7o;F;)ᜌWmn7MNBBBCy>}ޟsNۼ/}O'y=72z{N߼sO'޿z݉o]D' '+\2ݧpÆpbv''a}ݧbo'}+8HHHOO}П}O^}}{t7v~{wvuN)epB',NpppqN1=;?7;q;ynC NOxn:nI3|{'}u}r=؟}ޞON>=w}}3| N '?ݧG}' 8ZpN2s'2q=^WBNIp:NwWDpE??82pdt'nwy=]'IN}߾9_}܉~>ywwzy<[ݷG=^'ݧ4^'z}t+:Ӣ:-'INrޟ~}ス=ݧigRRRS ]"ȁh ?{x_{ܮu;rWߺ=?uuwwҽ:ӽ]_}~O2ݧzv/O?q/ҹ}inut^uWW__u_n׸[ҽ?uJ:W_tztW>>>?h S@"́h ?8{+yWOuMut_]~u~u7MuMyn}ӽ]7WWwWWwuu;{{WKu[Һu'_n۫]nt]^O{{^ a"Ёh >rn_W9{}}tn+nt'セޝ'vo'}to7W6::AW\}]MﺽW_zݷMtۧvܮurwW-ut9-<_z}ܷM_uu}uݧWNۦn}r_ $dۛnFo "ԁh >?ywMН?oa{ ##"?齺nɽon[WW/?oo77ܮfyn7Mw_woxy{wѽ^F齺^^_?]+{o77M wtN^oow{xwow B"؁h >?uu7_9))/tn[]=}u_}-{_~]tOuu]~W_u뺺9 '9IIIOM "܁h =uW/qwK^nJztޞ]^}߼o:WWWvޞW>»uWMunJtn]]-AD\dc_]oﺹ^{tz}ot'~\3{us]]  4h  !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb_ x,P `_ 2Yc(KgOp/a x,%d1 xX 2Yc B?%2~K<,~(Gd@Pg?d~K<,f~K<,}00#2Yc(0%d((3?%3?%Aќ,,|g|g/i~K<,i~K<,|H0#2Yc@0#2Yd<7A  !_(  xX  xY F`>GdƠ`>GdȘ5?%5?%DDQ,,&&`|g`|g1POtk~K<,k~K<,{X0#2YcX0#2YdTL   "`^  xX  xYF`>Gdư`>GdȘ5?%5?%DѬ,,&'`|g`|g0/tj~K<,j~K<,{P0#2YcH0#2YdGdƀ`>Gdho4?%4?%CCQ,,|g|g/g~K<,g~K<,}00#2Y|dAQ2Y|dA@Q%d AB?%YFPPd2 Ad AB^K %d __h 2Y@/%XF P,b x,0 Ac(Kd>р%d B^K oOO/{||~~ _8(?AANpp},,4< '8x@N&*&*q111?s{T\\\ # 8EN.*./qQQqpOS{TT\\ ⢢^8DD" p|4,44 a!_' 888HHABAN p?P0/|ߟ_ߠ_&7?o0/|$ !!'  8XXxCN"#p1?s{LLLT b^8FA=.226 q/3zddll "cާ8FF..67qqq/3zTL\d b^8C@N"p/{$,,, a 8(H/AOoϯ/}}|``?(((AB p|<?qSztt|t c^8FN..66qqq/SyTTd\ b"ާ8CDD@""ps{$4' _'7ٿ?p0?_󂃂~'!!g 8xxhoDN"&&'q11Q_s{Td\_# _'8FGN66:?qz||$$ ^!!8@N>>B>q3ytt|cc`^8GN.263qqQqS{LLTWb"b`^8xhCNppП| &8/AAAN pﳅ~4<GrSzd$$!!#!9 HB>BCr2/3zl ΁y B?Ì@=::>;qSz\\dgc?'8D"""#p{4,47``'8 pppӅ~<44??ߧ82..3qѱ|6> ge=xP:2|eƋ +/4)$$#HNBBBC2g#r21_rIB{NF $'%#%#,|d:I!#zARP/w]U@~f>A8EGܑеѿSydc4p|\\d_'8DDN""p|$,``'8HXH_Cp󇉉~LTLWb'8GG^;Ύw.7qbG|iGwU^>t1@ 0c@d K,X\hvƄ_4yt30|W_<9)~?$zC󬏼|rOEo> _Qȧ+5 MnM?P/47@0nOXɷd5U-=Gth>i a|yr?5ƢQ F92P6NGwRcP4 _l? %(?ػA}B|=+7xQ(ܓHn= @8\lt2w.6<U h?/ >7">7qphՁz~lltsc#c'8ȸEDN&""#q|4,4'ࠠߧ 8hhXgCD"&"%q1Q1/ӊ~\d\occc?ߧ8G^=܃oƐz Th~#b?ߴ%Z8dzs(d>4N~G<+d?߻d^4\ C_?9DK|Is~y6!e#n%KRW͙ӑO|`l``l_=J1@+y% hzyiģrmo\d G=U%FUS@< _>7y4|@li` #N:>>?q|\\\_b'8xBNppPP_4<<3"""8EN22.7qӎ~|||$?##ypf4mw*~-^ NHaພ#U- |~ph!L4*]< ٢@Ug%i_>6.acӟaYľ~4Ҭx4^lB+7vRS*y}P4v " )N/FV\|<߸ѲJj  .C44Vp{՟~||?'8FEN.&*'q01|4,4/ ' 8xxxDĿ&**%qQq1oӋ}\llsccߧ9 HHHF>FK9hIQ@ ܔOg)'))aI?=@ tNK/h?de%')-+,999ioCK4ﴂ@%㓇ioJKG-~Z[(p {rP))H ?')9Y)I/&JK~JKr22R?}|c8ȸȿEEN&"&#q}$$,'ᡟ8E&.21qqqӍ}t||#ߧ!!!!9 /IINJJJKrRRR_|$%?'%'))9IIIGJ?NVZZ[r|奥'----9iiioKNVRVSrs|䤤'%#%%9) /HNFF>?q{\\d_b'8xhxBCNp󇉈LLLK߿8F?>:>>?rӑ}䤤''%%%9))9?INRNNSr|奥e_'----9iiioeea#$|奥g----9iiioKJNVRV[rs{䤥$'%#%%9)HH>>BCqs{l\d_b'8xxCNp󈉉~LLT[߿8GGG?>>>=rӑ~|䤤''%%'9))9'JIJNRNRSrr|e'---/9iiyoF=\9Iy7D~~奥e'-+-+9iiYoJJRRNSrrr_s|dd'!!##8 N:66;qqs|\L\Kb"b?'  8HHHOCCC?&&&)qQqQo󌌌~\ldkccc_9 HHH?NBBBErRR2Oϓ|'%)')9IIIGJJJNRRRSr|߻dP `*](9iioKZZZWr|$%%%%9))/IHNFJJKr2|lllo#cc'8EDN""pӆLLLK8ȸƿ6::9rӏ~}dd'%'$9)))'IɿNRRRQrs{ee'++++9iiioegXj-?ޠz6=$ny----9YIYOINRRNSrR_|䤤%%%%9 GN:667qs|\L\Wb"b?'8XhhoC?&&&%qQqQo󌋋\d\ccc_8HH??NFJFIrRRROӒ|$')))(9IIIWJJNVVVWr|%%~<"z̻'-/-,9YYi_JJNRRRSr|ddd'#%#%9)/GG>:>;qѱѿs{l\l_'8xCCCN"""!q111/󊋊~\d\[#"A`h $ ~h` 9 ??NJJJIrRRROϓ|$g))'(9YYIOJJJNVVV[rҲ}ߧ-//.9yyioKKZZZ[r|$ߧ''%',|C{4qu?oӑ|cd?8EEN..*/qQ1Q?ﳉ}D...-qqqR)XW*@ s㣣㟟g#% 9'HI?>JJJIrRRoϓ~|e%$')'%+9YYI?KJJNZVVWr_ﳗ~奟'--'%9yiIoKKKVVVQrR}RN4䤤%%% ?NBFBCqg/ßSt|dOc"?ߧ,l\\1BN""&#qӆ~DDD;b_8ȷF>^-\Xh_ By{a~?/HK> rHDWH??18Hh|zEhHIH>xAG*4X;I@*Q.SM/s֬2Ĵ!M}Ñg;P3Q\$ҸzQNQB翾YÎ;ß?ڡPʎV&I2 _|޾ $i9ƿ>R!rs O<sX~rƥAZ? xfOCM{>7|3DDLC"""?' 8xĿ?*.*-qqq󋍍}~j4Ѭ@?AYjha!mtZ9qZ@" ܓ yhdW}}V{SA rrrROϷME<|i@=6@(yFhe~PzQyyI?>xs~4-=ɿARbVUKVi7~{c_,I#_eF@ y(IE9*4 *O###"RUYGH> j/sYsҕw4ϋ}}C^8DN"""#pLLLK"81@  +TA{P=qwH.5\i P4rM=?A84 @@=ǩ@9)) 7)R)B>`=4@$ yFhdہPԂAyJJJ?==!QOunM7IKJ?h 4sc_(?M?E|3ɮ@  =,@&ʽy.6> k>j:kayW^F)Q}ʁy/6yHH`hT{h4iGH?::B?H]\i cpa^EE?*&*!q|BBAqƠ㏕}@4r*{|UO?QBs_)#2Pyʍ=T. ߡ ܚ4~`4 ??rQ?/rro=ʍ@4 Vh|~v 38k~7 *4mw~~̴ ?>bA`* t ܒ4~j(3>g 9I9IG2@ rH/5_u"?=otXyߡ䐎|jYOHSC[8G F?_]rf #{ᢁ|\\W⢢'8xCCBN"""!qQqQo󌌋dldk##c_ dc|ts$( 䌋 A|$d__>y7K|q^)'?? 윓!A%$4Ҳϳ~{A)*(KJ&.^U~~a?A#+) ɿ؃r󒓑|6 dީ I9'"HR@ ~ttq# (>>>?X3O{tO ""'8CCNq/󉊉~T\\[###8G?NBB>Ar2Ӑ|䤤'%%%%9)99OJ?NRNRMrrrR|%$'++++9iiioKKKNZZZ[rҲ|%%$ާ''''9)))/INJJJGrRR2?Szd$$ާ!#!xh?Ai׺_8N"""'qﳆ|LDDC⢢߿8F??N6>69rӏ~~|$$$_'#%#%9)))'IH?NJJJIrRRR_{䤤%%''9999OIRRRSrs{%%%?))))9IIIOIIINJJJKrRR2_Szd$d?#!##8:6:;qѿӎ{d\l__'8xxC@N&&"#q111?󊊊\d\[#c#?ߧ8N>>>?r?|䤤'%'%'9))/IIIJJJKrRRR_{ާ''')9999/INNNJOrrrRSz䤤ާ%%%%9))/NFBFCqSz|||㣣'82.23qqQ0/{D<<< _' 8EE222/qqӌ~ttlwc'9 IHHNFJFKrR22_|䤤%%%%9)))/IINJNJKrrrR_Sz䤿ާ''%%9)9)/INNNJKr22R_Szd$ާ!!%!9 GGNB>:;q|ttloc#b'8ED@N""p/DD:>:q/Szllll cb_'8D@N"""q/|44$$ ᡡa8xED*.*+qqQﳍ}ltdocc#'8GN>>>?rs||$#'9 HGNBB>CrSz$d$?ާ!!!9 HHHNBBBCrSz|||w㣠^8F@N6622qqqQP/|LTDD b"!^ 8hxhXB@ pp󆇅<4,/!?8E.22/q|dldgccc'8GGN>>:;q/|tttw㣣'8HGGNBB>>r/Sz|| $#^8G@N>6:6qё/Szldd\ ⢠^8DD@N"""q/|4,,$ ! _(&PP Y  0[k000[o`Y Z  8 Z 8 [hv`!`֐`!`D??f&)q-q-1qMCoCo﹋m~ |-m~ |-}\Sh00[kh00[obA [ s 8 Z 8 [`!``!`ƶ??f65q-q-1ѭCoCon~ |-n~ |-|lsp00[kp00[l cc [ 3 8 Z 8 [`!``!`F7??6=q-q-1CoCo/yo~ |-n~ |-{lsx00[kx00[l cc [ `^ 8 Z 8 [`!``!`F??65q-q-1mCoCo/ym~ |-m~ |-{\Sh00[k`00[l bA Z `^ 8 Z 8 [`!`ְ`!`D5??fq-q-0 CoCo/h~ |-i~ |-|4+H00[k800[l aa Y@?f 0[k((`3~ |-e l ? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@C4 "h =t]-ҽ:WIt/ޯ-ѼW]v/on]_W]'ޮݷIw}tOin}ޟv[t[y^nt/ݫKtJ:NӴ^'Kzv'J>N]'܏rw3v]N_߽;t=oWn'j;]:Nĝ+] НztWO $)"h =+{w}ӧt]]/K]޷JwztwtuK޻_=[ӱ===ntt/z{[u/ GK"h =?w۷ww~wn۶]セwkzzzxSWiwwzvzv޿iݟzwvzvӴ;_vݞ~ww/_~w׻NI]IR <l"h =?_;W=]{~ nw;ӷx?￾ۧwwwݷWr}_=kvovwwvwݿ{G "h ޿sn{シ}N;w}{zswi۫o{wrss;sn۶w';;ws߹W;7W﻾{}߻>wﺻO}}7wzw}サޟ{ܟ C"h =?{wnۻݧmݷWj~oNýy]iwmӵzW{svܟwwݽvw}}wwvnNw_N {"h =?w߽ٻ{w{umﹷw+}o'wy[yݷ;swܮspwݿ}k{۷|{gw{ڻww6cՕo{}}ۿmݾ=/ "h =wwݿvݿn{۶ﭽo's}wwwnW}} w7wݷov_wݿsww?Zݷmۻv݅{www_;ݾ{Wݷaok{ݾv $4"h =?ww?;ݵwzww7oO vnvݷo{<wwv{~o/ V" h Gd0`>Gd1?%2?%?є,, |g|gPP/f~K<,f~K<,|00#2Yc80#2Yd$$  A !a_(  xX  xY F`>Gdƀ`>Gdhh4?%4?%CC@Q,,"@|g@|g/j~K<,j~K<,|X02YcX02YdLL   "b`^  xX  xYF`>dư`>dȘ5?%6?%D@Ѭ,,*&`|g`|g10/tk~K<,k~K<,{X02YcX02YdLL   "b`^  xX  xYF`>dƐ`>dȈ4?%5?%DD@Ѥ,,|g|g/h~K<,h~K<,{@0#2Yc@0#2Yd44   !a _(  xX  xY F``>Gd``>Gd((3?%3?%AA@Q,, |g|g00/d~K<,c~K<,|0#2Yc0#2Yc    _(  xX  xXE`>Gd`>Gdǧɽ==o/~ ``_g88HHHBBB@Np/|<>q/Szd\|| 㣠^8ȸN.&6.q11qpOSzDDLL b`_' 8hhhhCNppPП|`&7 pPPӃ}$447ᡡg 8N&&./qQqq|\\locc'8GN>>>CqSz| $ ^!!8 HN>>BBq/Sztltt cd#8EN&&./q11Q_|44DG"?'  8hHh_ACBN p00P?| ``  8XhH_CCNpӈ|LLT_'8FFN66:;q|d"=I?HHHNBBBCr220/Sz $$$!!#!9 HHHNBBBBXz0TxG9$||K'8N.../qqqqs|LLLOb"b' 8hhxoBCBNpPPp?~ ࠠ 8XhXoCCCN"#q11?ӊ|\\\_?'8FDN6>>?q|4⌂AO,@ a$$cHBBBCY S{ Ԓ2'!#!#9($dbGBBJF7v-$.4V^kQ|Yc@z;qёѿY={qqqq|LLTWbbb'8hBNpPPpӁ~ ? 8hhhoC"&"'q111?Ӌ}\\\gccc'9\EJZ/.q#;+c^F?5eMqOT'pa" h?߹X49aaHxOMy :GQxwɳ^9!CcL.@,|`QX 1 ?PA4nOTzDYqqC;t>_ ]ư @yȅ!0~}ÙQ*? dJ5ΕCД?7xxn: ޹qsGL\h~kHZn3q|TLTObbb'  8hxhBBNpPPP_ӄ,4,7ᡡ8DN&*&+qqqﳍ}lllo'Th~܃܇qj.1r%`Cd* ߪ$h X~Kި ďU@La!ܠ:/M;Ϳw4?N}C4@0h$?$%j]luh?$C.Cq>CMa('.mIpۓx|8{ƊK+ rP/9 FFx>+6=O^B!B>Aq?,qc BCLU~nEKtDQ6X>i:dP7(أ5U@h#HI@ AŝDc4^osq ~42&\EXuMQ%(2z4u8| cG40Qpk&\nQ x x~WCo\X`YySrQ?7 ćߚ|/65M6wP;x %Th~hx27SqTo'8ȸȿEN&"&#q|,$$'ߧ 8hhhgC?"&"%q1Q1O󊋋~\d\ccccߧ8G?N>FBA#}uѪh߬bPA ??1@ 3Ɗ|g߱<6/\>A(y/1P4Pe`>@X~So ՟}t6z> _+=)ԅ`uKzWk>;j9Kl6Z>540Q#y78UF8d _,ar0ܤ>pnuT񗁠||T 0 Wcx/!5.ꏮ@MA gê\MUӋXf[ON>:BCq|ddd_'8CCNpPpP_ӆ4<_%$F%%%?'#%))GȬX(_''FP%%?g+-+-H~io9Y_JK+grﳔ{4奿'')))4x7P }|r[䤿hoJK(R/|||㣣'8ȸȿN&&#p},ᡡ8E...5qqӍ~lt|g!!!!9 )/II?NBJJIrRRRO|%%%?'%%))99IIOKKNVZZ[r|奥'--//9YYioKKKNRRZ[r|䤤'%%%%9))/HHHNBBBCq|d\d_b'8xCCBp󈈈~LTTK߿8F?N>>>?qﳐ|䤤'%%%$9)))7JJJNRRRSr|奥'--//9iiye#$~}'----9iiioJNRRVSr|䤤'%%%#9 HHHN>>>?qѱ|\L\Wb"'8hXh_BBBNq111/󊉊~T\\[#b_8ǿ?NBBBArϳ~}䤤'%)%$9II)OJJJNRRRSr|'//-/yxH~~&rrp9=󗗓|奥'))--9YIYOJJJNRJROrRrr_|$$d'!!!!8FN2.23qq1?|LJJJIrRRROϓ|$?'))))9IIY_JJNZZZ[r|sY9| #|X` |0<9)yiKKK?NZZZ[rҲ|$$'%%%%9)))/IIINBBBCr|tltobb'8DNpﳇ~LLLK߿8ظƾN6:69qӐ~~䤤%%%$9)))/JIJNRRRQr|奥'----9yyyKKx@ } v '$;woo C׼BZ^Z]r|%%%?'))))9I)I/IIINJJJKrRRR_|'8EEN.&.%q111/}4443"""8ƿ6665qѱӍ~~|||{$$$!!# 9)))'III?NJJJKrRR|%%%'----9iiioKKKN^^^_rԆij"&Z](o||奥'+)))9IIIOIJINJJJKrRRR_|䤤'!!!!8FFN6.6/qqqqo|LLLK!!g 8ľ*.*-qqq󌍎}cb?wh!"cAVmqϳ~}䤤%%%$9999'IJ?NRRRSr|奥g----9yyyLK˿N^^^_r|奥'----9IIIOJJJNRNRSrrR_|1'ǘ%!! 9 HHH>>>?qѱѿ}\\\_b_ߧ8xxxCCC""q1Q1O󊋊}\l\kcco?hl 'fa9 H?>JFFMr2RRϓ~|%$?'))))99IY_JJJVV^WrҒӗ~Ĵ奟g///.9yygKIZVNWrҲ|d$?g)''%9)9)AϞ?9f?#N>:>3q}dd\Gbaߧ8xhh_CBB"!q1Q1/󊋊}\d\[c##_q~6́~VlCuuj` ~`AhϹ H ~$ ݀qMB+6 9)))'Ǿ rushH{%e$t2Cz {:Hi2qjˎ2vPcA5*\$/扇{>cɟ} h OSՍߠ)(mA0JuG=>ۤs 4@\=$^EQQ0}@J.z4_𓑏{;ZAAЁ?P w 4WjĹ z4qnrqqqo?_ZVrq01ӆ|$$47"""8F2269t3h`uh`ͿDN:B>A S㜇ߠM"F+ .~qx5Fa38@'&&%q1Q?|4447"!_8FF6:697 ?sC^l=~t{܇߿z(lX40Qt+ܔ/??D Lߞ&>7&\m+ 9{E#|nN. ~l<ͿCRRRQ켫h4ҍ?(Ԓ@ԃC+-+(\\ Kna~nQ rS=OH;=AqXg$wTGs> _͞h`>mה#!Ά'EF')''y8eF}6vb YYGGhAz[\HGȿ Q  gt/w3G1D|ƻ*-( ;skp_%$U>|8O~jyaڸuQ~7$\;͚x@ϰ󔔐~~7& 4qt!睫>wNVVRQǹ(8QcNcrˣG= |lX40Pys6nQXE~ܳqcβ_ +%=++ =wDTi~lyӔ|qE4A{%~+@\q$T|<DE_9=+7Ot|l"c:p JsN&&&'q|DDDCbb8?>z:9}g㟡"||sqN0|{?! 8 F ܔu%"H ?#&GPh?!@&NM8Ғoϔu7OI?I)7EǻA+*GPh>/J??RRRYS$ߨ k%h ʿ_#-'G E?㐃}NW qRRoA'%G>'d㎈}4~GE~FF?9I>A8cch/{\8ȸȿN&&&'q|DDDCbbb_8GGG??6>6=qӏ~~$$$'%%%$9)))'III?NJJJMrRR|$'))')9IIIOJNZZZ[rҒҟ|%%%?'))))9IIIOIIINJJJKrRRR_Sz$$$ާ!!!!9 GK eǕ3>N.&.'q111?|4447"""8FF6665qѱӏ~~|||{$$$g!!! 9)'III?NJJJIrRRRO|䤤'%%%%9)))/JJJNRRRSr|%%%?'))))9)))/JJJNJJJKrRRR_Sz$$$?ާ#!!!9 HHHN>>>?q~l\l_ާ8CCNp󉊉T\T[8F?N>>>=rӐ|䤤'%%%%9)))'III?NJJJIrRRR_|䤿'%%%%999)/IIJJJKrRRR_Sz䤤ާ%%%%9)))/IIINJBJCrSzާ8FFN6.6/qqqq|LLLO!!' 8xxhgDD*.*-qqqqo󋌋~lllkccc_ߧ8HHH?NBFBAr2R2_|䤤'%%%$9)))/IIINJJJKrRRR_|䤤ާ%%%%9)))/IIINJJJKrRRR_Sz䤤ާ%#%!9 HHHNBBBCqS|tttw#c"'8N&&&#q|4447a?8FF66.7qӍ||||#'!!!!9) HNFJBGrRR2O|䤤'%%%%9)) IHHNJJBKrRRR_Sz䤤ާ%%%%9)))/IIINJJFCrRRSz$#ާ!8GFN66..q|\LLObbb _'8xhhoCBBNp󉉈L\LO#"8FN:>67q||$#?'!#!!9 IHHNJJBCrRRz$$$?ާ!%!!9)) IHHNJJBCrRRSz䤤$?ާ#!!!9 HHGNB>>?qSzllll cb_'8N&""q/|44$'! ' 8xhhoC&&'q1q1?󋌉~ll\_cb?'8N>>67q||$#'!!9 HGNBB>?rSz|$$$?ާ!!!!9 HGNBB>?rSz|w#ާ8FF@N662.qqqQP/|TTLL bbb?' 8hxhhCB@N pPPP_44$/ᡡ! 8xDN..*+qqq|lldgcc#?'8GN>>::q/|||tw㣠_'8G@N>B::r/Sz||| 㣠^8G@N>662q/Szlddd _'8DN&&""q1/|4,4, a!a _'8((((A3?? q-q-0pCoCoh~ |-i~ |-?no o{۶O{uu{utuuut'Wo+wo{}7w=sWcym̝;oWw+onFwo;ܛ{{tNuynu_unooW u{ntzt7=s ?"(h >?{{{~C?_w a",h =?׽o/u?Nuo?ܿ O"0h ~ݷfmuw{Nya^T3}ﻆqѱowiongӻi3zaxwݻ;~ݍgm[ Mnwﰯv[z| B"8h