The manual tests for cppcheck-gui are written in Gherkin, the plain-language Given/When/Then format. gui-test turns them into automated tests: Claude explores the GUI once and writes a script, and from then on the script is replayed without any AI.
+
Updated 2026-10-24
+
+
Overview
+
Working with gui-test has three steps:
+
+
Write the test in plain words: what the starting point is, what you do and what you expect to see.
+
Generate the script, once. Claude does the test in the application and writes down what it did as a script.
+
Run the tests as often as you like. The scripts are replayed without AI, and you get a report with the results and screenshots.
+
+
All of this is done in the web interface. The application runs on its own virtual screen, so your desktop is not touched while tests run.
+
+
+
+From a test in words to a report: gen.py once, run.py every run.
+
+
+
The tests are kept in tests/, and every run puts its report, logs and screenshots in a new directory under results/.
+
+
Getting started
+
Start web.py with the cppcheck-gui binaries you want to test, then open http://127.0.0.1:8080/ in a browser.
--opensource and --premium each take the binary of that edition. Give one, both or none; without a binary you can edit tests but not generate or run them.
+
--src is the git checkout the binaries are built from. It is only used for the git SHA, branch and commit in the report; the default is the directory of the binary.
+
--dir (default tests), --out (default results) and --port (default 8080) change where test files and results live and which port is used. The server only listens on 127.0.0.1.
+
+
Some packages must be installed first. On Ubuntu (checked on 26.04):
The tesseract packages are only needed for ocr checks. To generate scripts the claude command line tool must be installed too.
+
+
The web interface
+
+
Header: Save, Run all (every test in every file), the Edition picker, a status message and a link to this handbook. The Edition picker only shows the editions that web.py got a binary for, and "both" when it got two. Without a binary there is no picker, and tests can be edited but not generated or run.
+
Left column: three boxes.
+
+
Test files, with New file. A file is flagged invalid when a test in it has errors, and regenerate N when N tests have an outdated script.
+
Test projects, with New test project.
+
Results: Reports lists the earlier runs with their date, edition, version, test files and result. Click a date to open the report.
+
+
Main area: the open test file with its tests, the open test, or the open test project. Each has an ✕ in the top right corner to close it.
+
Job box: while a script is generated or tests run, it shows the progress, the results as they come in and a link to the report. Stop ends the job. Its ✕ hides the box until the next job.
+
+
+
Gherkin
+
The manual tests are written in Gherkin, the language that Cucumber, behave and other behaviour-driven development tools use. Gherkin describes behaviour in plain words with a small set of keywords, so a test reads the same for a developer, a tester and Claude. Each test file in tests/ is a .feature file with one feature, and every scenario in it is one test.
+
Feature: Preferences
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t28ce8afe @project:embedded @edition:opensource
+ Scenario Outline: Misra rule text file
+ Given Edit > Preferences is open on the Addons tab
+ When I select the rule text file {project:embedded}/<rulefile>
+ And I close the dialog with OK
+ And I open the project file {project:embedded}/embedded.cppcheck
+ And I wait until the analysis is done
+ Then the Misra error for the unused parameter has the rule text from the file
+ And there are no internalError results
+
+ Examples:
+ | rulefile |
+ | rules.txt |
+ | rules with spaces.txt |
+
+
Keywords
+
+
Keyword
Meaning here
+
+
Feature:
The first line of the file. Its text is the title of the test file, shown in the editor and the report. Lines below it, before the first scenario, are a free description
+
Background:
Steps that come before every scenario of the file, typically Given the application is started with no project loaded. At most one, before the scenarios
+
Scenario:
One test. The text after the colon is the name of the test, unique within the file. Example: means the same
+
Scenario Outline:
A test that is run once for every row of its Examples table. Scenario Template: means the same
+
Examples:
The table of a scenario outline. The first row is the header with the column names; <column> in a step is replaced by the value of the row. See Examples (test matrix)
+
Given
The starting point: the state that the application is in before the test does anything
+
When
An action of the user: a click, a menu entry, typing, opening a file
+
Then
The expected result: something that can be seen in the GUI or in a file. Every scenario needs at least one Then
+
And, But, *
Continue the step above with the same meaning, e.g. a second Then. A scenario can not start with And or But
+
"""
A doc string: lines of text that belong to the step above, e.g. code to type into the scratch pad. It ends with another """
+
| a | b |
A data table that belongs to the step above
+
@tag
Tags on the line above a scenario, see below
+
# text
A comment. Comments are lost when the web interface saves the file
+
TODO:
Not standard Gherkin: a description line under the scenario name that says why the test can't be automated yet
+
+
A scenario can switch between When and Then more than once, when a result has to be checked in the middle of the test:
+
Scenario: Filter the results
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I type "arrayIndex" in the Filter field
+ Then only the arrayIndexOutOfBounds error in b.c is shown
+ When I clear the Filter field
+ Then all errors are shown again
+
+
Tags
+
Everything about a test that is not a step is a tag on the line above the scenario. Tags can't contain spaces.
+
+
Tag
Meaning
+
+
@id:t1a2b3c4d
Identifies the test, also when it is renamed. The script is stored by this id. Added automatically when it is missing; don't change it
+
@ticket:14893
The ticket of the test, shown as #14893
+
@project:NAME
A test project that the test uses, one tag per project
+
@edition:opensource
An edition that the test is for, one tag per edition. Without the tag the test is for all editions
+
@disabled
The test is not run
+
+
Other tags are kept but have no meaning for gui-test.
+
+
Writing good steps
+
Claude reads the steps as they are when it makes the script, and the Then steps become the checks. A good scenario is one that a new tester could follow without asking:
+
+
Use the words of the GUI: the menu entries, button texts and dialog titles as they are shown, e.g. When I open File > Edit Project File.
+
Write Given steps as a state (Given the analysis is done), When steps as an action (When I click Recheck) and Then steps as something that can be seen (Then the nullPointer error in a.c is gone).
+
One thing per Then step. Then there are no results for a.c and And there are no results for b.c become two checks, and a failure points at the right one.
+
Be concrete about results: name the file, the error id and the line instead of "the results are correct".
+
Say how to wait: And I wait until the analysis is done.
+
Refer to files with {project:NAME}/path, and to the scratch directory with {work}.
+
Keep the steps that all tests of a file share in the Background.
+
+
Changing the steps, the Background or the Examples makes the script of the test outdated, because the script was made for the old words.
+
+
Differences from Cucumber
+
+
There are no step definitions. In Cucumber every step is matched by a pattern to code that someone writes. Here Claude reads the scenario once and writes a script for it, and the script is replayed. Steps can therefore be free text; they don't have to repeat fixed phrases.
+
The script of a scenario is stored next to the feature file in FILE.scripts.json, by test id.
+
Rule:, tags on Examples, and more than one Feature per file are not supported.
+
Placeholders in braces are gui-test variables, not Gherkin: {project:NAME}, {work} and {edition}.
+
TODO: description lines are a gui-test convention.
+
+
+
Writing tests
+
A test is a Gherkin scenario, written as if for a person: Given steps say where the test starts, When steps what is done and Then steps the result to expect. Claude reads exactly these words when it makes the script, so be concrete.
+
Feature: Results list
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t389a5f3a @project:basic-project
+ Scenario: Clear results
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I click Clear results
+ Then the results are cleared: the result tree is empty
+
The files can be edited in the web interface or in a text editor. The Gherkin chapter describes the format; the web interface shows the tags of a scenario (ticket, editions, test projects, enabled) as separate fields.
+
Tests live in test files (tests/*.feature), one file per area of the GUI. Click a file in the left column to open it, or New file to create one. An open file shows:
+
+
Title: the Feature: line, shown in the report.
+
Background: the steps that are done before every test of the file, e.g. Given the application is started with no project loaded.
+
Tests: the list of tests. Click one to edit it, + New test to add one. A test is flagged invalid when it has errors and regenerate when its script is outdated.
+
Run tests: runs all tests in the file.
+
+
Nothing is written to disk until you press Save; a dot after the file name means there are unsaved changes. Errors, such as a test without a Then step, are shown under the field and must be fixed before you can save.
+
The test editor has these fields. The ↑ and ↓ buttons move the test in the list, and Delete removes it.
+
+
Field
What to write
+
+
Name
Unique within the file. Shown in reports and the history, so a rename starts a new history line
+
Ticket
Optional, e.g. a Trac ticket number
+
enabled
Untick to leave the test out of every run without deleting it
+
Editions
Tick the editions the test runs in; by default all. The script is the same for all editions, {edition} is replaced by the edition's name
+
Scenario steps
One Gherkin step per line, e.g. "When I open File > New Project File". Every Then step becomes at least one check in the script. Use <column> for the values of the Examples
+
TODO
Why a test can't be automated yet, e.g. "cannot be automated". Leave empty otherwise. In the file it is a line TODO: ... below the scenario name
+
Test projects
Tick the projects whose files the test uses, see below
+
Script
The script of the test, with the buttons Generate script, Run test and, when it is outdated, Approve the script. See Scripts
+
Examples
A Gherkin table; every row is a separate run and the test becomes a Scenario Outline
+
+
+
Examples (test matrix)
+
A Scenario Outline is run once for every row of its Examples table:
+
When I select the platform <platform>
+ ...
+ Examples:
+ | platform | lang |
+ | native | C |
+ | unix32 | C++ |
+
That is 2 runs. A step like "When I select the platform <platform>" gets the value of each run, in the script it is {platform}. The runs are named after the test plus the values, e.g. Relative paths [platform=unix32, lang=C++], and each one gets its own line in the report. The editor shows how many runs the Examples give.
+
+
Test projects
+
A test project is a directory of files that tests use: source files, a .cppcheck project file, a .cfg file. They live in tests/projects/NAME and are edited under Test projects in the left column. Before every run the project is copied to a new empty directory, so a test always starts with the same files, even if an earlier test changed them. In a script the copy is {project:NAME}, or {project} when the test has only one project.
+
Changing a file in a project makes the scripts of the tests that use it outdated, because the script was made for the old files.
+
+
Scripts
+
A test runs only when it has an up-to-date script. Generate it with Claude once, check it, and from then on it is replayed without AI.
+
+
Generating a script
+
Open the test and press Generate script. gen.py starts the application with the test projects in place, and Claude does the test by hand: it looks at the accessibility tree, clicks and types, and checks the expected result (the Then steps). It then answers with a script. The script is replayed from scratch and saved in FILE.scripts.json only if that replay passes. When the replay fails, Claude is told why and tries again, 3 attempts in all. While a script is generated the editor is locked.
+
+
A Scenario Outline is explored with the first row of its Examples. The script uses {column} where the values belong.
+
When the application does not do what the expected result says, Claude still writes the script that checks it and marks that one check "observed_fail": true. The script is saved, and the test fails until the bug is fixed. That is a finding, not a broken script.
+
When the test can't be done through the GUI, Claude answers CANNOT: <reason> and no script is saved.
+
+
Read the generated script before you trust it, especially its expect steps: they decide what the test proves.
+
+
The script format
+
A script is a JSON list of steps. Each step has do (an action) or expect (a check), and a label in the words of the test that is shown when it fails. Elements are found by name (part of the accessible name, case-insensitive) and role, and if needed path, nth and exact.
+
[
+ {"do": "click", "name": "Help", "role": "menu item", "label": "Open the Help menu"},
+ {"do": "click", "name": "Contents", "role": "menu item", "label": "Choose Help > Contents"},
+ {"expect": "visible", "name": "Cppcheck GUI help", "role": "dialog", "label": "A help window opens"}
+]
+
+
Step
What it does
+
+
do: click, rightclick, doubleclick, focus, hover
Act on an element; "mouse": true clicks with the mouse
+
do: settext
Replace the text of an edit field with value
+
do: type, key
Keyboard input to the focused element; keys uses xdotool names like ctrl+o, Return
+
do: wait
Wait for an element to appear, or with "gone": true to disappear
+
do: restart
Quit and start the application again; settings are kept
+
do: sleep, write, copy
Pause, write a file in {work}, or copy a file in a test project
+
expect: visible, gone
An element is shown, or is not
+
expect: state
An element is or is not checked, enabled, focused, selected, expanded...
+
expect: value
The value or text of a field equals something
+
expect: text, ocr
Text is (not) on the screen; ocr reads pixels with tesseract, for context menus AT-SPI can't see
+
expect: file
A file contains, does not contain or matches a regex
+
expect: alive, exited
The application is still running, or has quit by itself with status 0
+
expect: capture
Search regex in an element and save group 1 as a name; {name} in the later steps is replaced by it, for text that changes such as the version
+
+
Checks and actions wait for their element up to timeout seconds, 5 by default. The placeholders {work}, {home}, {project:NAME}, {column} (of the Examples) and {edition} are replaced before the run, captured values when the capture step has passed. The full description is under How to write a script below the script in the editor.
+
+
Outdated scripts
+
A script is made for the test's steps, the Background of the file, TODO, Examples and the files of its test projects. When any of those change, the script is OUTDATED: it is kept but skipped by every run, and the editor says so in red. You then have three choices:
+
+
Generate script again.
+
Approve the script when the change does not affect it, e.g. a typo fix in a step.
+
Edit the script by hand in the editor; it has to be valid JSON before you can save.
+
+
+
Running tests
+
Every test starts a fresh application with an empty HOME and fresh copies of its test projects, so tests never depend on each other.
+
+
From the web interface
+
Pick the edition in the header first: opensource, premium or both. Then:
+
+
Run test in an open test runs that test, all the rows of its Examples.
+
Run tests in an open test file runs every test in that file.
+
Run all runs every test in every file.
+
+
With "both", the run is done for one edition and then the other, and each edition gets its own report. The job box shows the test and step that is running and the results as they come. Stop ends the run at once: the test that was running is SKIP "aborted by user", and the report is written with the tests that are done. Only one job runs at a time.
APP is this edition; only the tests for it run. Without it all tests run and there is no history per edition
+
--test TEXT
Only tests whose name contains TEXT (case-insensitive)
+
--id ID
Only the test with this id, all the rows of its Examples
+
--src DIR
Git checkout for the SHA in the report; default the directory of APP
+
--out DIR
Where the run directories go; default results
+
+
run.py prints one line per test and the path of the report, and exits with 1 when a test failed, so it can be used in a script or CI job. Scripts are generated the same way with ./gen.py APP FILE.feature; it takes --edition, --test, --id, --force (also tests that have a script), --model and --attempts.
+
+
What happens in a run
+
+
The tests to run are selected: enabled, for this edition, not filtered out. Tests without a script, or with an outdated one, are SKIP.
+
For each test, the test projects are copied and the application is started on a new virtual screen with an empty HOME.
+
The script is replayed step by step. After every step that acts on the GUI (click, type, key, settext, restart...) a screenshot is saved.
+
At the end, or at the first failing step, the step log, a final screenshot and the application log are saved, and the log is searched for a crash.
+
After the last test, results.json and report.html are written in the run directory.
+
+
+
Reading the report
+
Each run writes one self-contained report.html; read it top-down: the verdict first, then what changed, then the details of each failure.
+
+
+ The top of a report from three runs of two test files.
+
+
+
Part
What it tells you
+
+
Header
FAILED or PASSED with the count, and the run details: date, edition, duration, git SHA ("+ uncommitted changes" when the checkout was dirty), branch, commit, application, host, test files
+
Number boxes
Passed, failed (how many crashed the app), skipped, duration, and with history the change against the previous run
+
Results per test file
A bar per file, green/red/amber. Shows whether failures are in one part of the GUI. Only when the run has more than one file
+
The last runs
Pass, fail and skip counts for up to 15 runs of the same edition; the number on a column is its failures
+
Changed since the previous run
NEW failures, tests that are fixed, and tests that are STILL FAILING
+
Status history
A row per test that did not pass in every run: "new failure", "failing since 10/22 12:00 (b8b27a)", "fixed since", "flaky: 4 changes", "never ran"
+
Failed and skipped tests
A card per test: the reason, the step strip, the screenshots after the last 3 steps and the final screen with a red frame
+
All tests
Every test grouped by file, with its time, links to its files and its step screenshots
+
+
+
The step strip
+
Every test has a strip with a cell per script step: green passed, red failed, grey not run. A failure at step 2 of 14 usually means the test or the application broke early. A failure at the last step usually means one check is wrong, or the bug the check describes. Under the strip it says e.g. "failed at step 9 of 14", "not run" or "did not start".
+
+
Files in a run directory
+
A run directory is results/<date>-<time>-<edition>-<sha>/, e.g. 20261022-120317-opensource-b8b27a95/. The files of a test are named after the test (NAME below), with spaces and other special characters replaced by _. They are also linked from the report.
+
+
File
Content
+
+
report.html
The report
+
results.json
The run details and every result. The history is read from these files
+
NAME.txt
The step log: each step, ok or FAIL, its time and its screenshot
+
NAME.png
The screen at the end of the test, or when it failed
+
NAME.stepNN.png
The screen after step NN, for steps that act on the GUI
+
NAME.app.log
What the application printed
+
+
The history compares runs of the same edition in the same results directory. Tests are matched by name.
+
+
Troubleshooting
+
Start with the reason in the report and the final screenshot: together they usually say whether the application, the script or the test is wrong.
+
+
What you see
What it means and what to do
+
+
SKIP "no script"
The test has no script yet. Generate one, or write it by hand
+
SKIP "OUTDATED SCRIPT"
The test or one of its test projects changed after the script was made. Generate again, or approve the script if the change doesn't matter to it
+
FAIL "timeout waiting for element to appear"
An element with that name and role wasn't found within the timeout. The reason lists the most similar visible elements and the open windows: a renamed button shows up there. Fix the name in the script, or regenerate
+
FAIL with observed_fail=True in the reason
Claude saw the application not do what the expected result says when it made the script. It is a known bug, and the test fails until the bug is fixed
+
FAIL with CRASH
The application was not running at the end of the test, or its log has a fatal message (SIGSEGV, SIGABRT, an assertion, AddressSanitizer, QFATAL...). A crash fails the test even if every step passed. Read NAME.app.log. A test that quits the application with status 0 is not a crash
+
FAIL "could not start"
The session or the application did not start. Check that the packages under Getting started are installed, and that the binary runs
+
"flaky" in the status history
The test changes between PASS and FAIL. Usually timing: give the slow step a longer timeout, or add a wait step before it
+
A context menu can't be found
Some menus are not in the accessibility tree. Check them with an ocr step and choose with key Down and Return
+
"path ... is outside the test directories"
write, copy and file steps may only use {work}, {home} and the test project copies
+
Generate script or Run test is greyed out
web.py has no binary for the edition picked in the header, or the test is not for that edition. Run test also needs an up-to-date script
+
+
+
Reference
+
The commands, with all their options. web.py, run.py, gen.py and guitest.py also print their help with --help.
guitest.py is what Claude uses to drive the GUI, and you can use it by hand to look at the GUI the way a script sees it. The session lives in $GUITEST_STATE (default /tmp/guitest-<uid>/state.json).
+
guitest.py start ~/cppcheck/build/bin/cppcheck-gui
+guitest.py tree --showing --depth 3
+guitest.py click --name Help --role "menu item"
+guitest.py screenshot /tmp/screen.png
+guitest.py stop
+
+
Command
What it does
+
+
start APP [ARGS]
Start Xvfb, D-Bus, AT-SPI and the application. --home DIR (default an empty directory), --size 1280x900
+
stop, restart
Stop everything; quit and start the application again with its settings kept
+
alive, log
Exit 0 if the application runs; print what it wrote to stdout and stderr
+
windows, texts
The top level windows and dialogs; every visible text
+
tree
The accessibility tree. --showing only visible elements, --depth N, --path P one subtree
+
click, rightclick, doubleclick, focus, hover
Act on an element found with --name, --role, --path, --nth, --exact. --mouse for a real mouse click
+
settext VALUE, type VALUE, key KEYS
Set a field's text; type into the focused element; press keys (xdotool names)
+
wait
Wait for an element, --timeout 10, --gone to wait until it disappears
+
info, suggest
Role, name, text, value and states of one element; the visible elements most like --name/--role
+
ocr, screenshot FILE
Text on the screen read by tesseract; a PNG of the screen
+
+
+
+
+
diff --git a/gui/manualtest/docs/report.png b/gui/manualtest/docs/report.png
new file mode 100644
index 00000000000..2a24756da06
Binary files /dev/null and b/gui/manualtest/docs/report.png differ
diff --git a/gui/manualtest/gen.py b/gui/manualtest/gen.py
new file mode 100755
index 00000000000..0a3360065dd
--- /dev/null
+++ b/gui/manualtest/gen.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+"""Let Claude write the scripts of the tests. The scripts are replayed by run.py without AI.
+
+ gen.py APP FILE.feature [--edition opensource|premium] [--test SUBSTRING] [--force] [--model M]
+
+For every test without a script (or every test with --force) Claude gets the scenario, drives the
+GUI once with the guitest.py commands and answers with a script, see testfile.SCRIPT_DOC.
+The script is run again from scratch and is saved in FILE.scripts.json only if that replay passes.
+A Scenario Outline is explored with the first row of its Examples, the script uses {column} where
+the values of the row belong. With --edition APP is that edition, tests that are not for it are
+left out; the script is used for all the editions of the test.
+"""
+
+import argparse
+import json
+import os
+import re
+import signal
+import subprocess
+import sys
+import tempfile
+
+import runner
+import testfile
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+GUITEST = os.path.join(HERE, 'guitest.py')
+
+PROMPT = """You are writing an automated GUI test script for a Qt application that has been started for you.
+First do the test by hand, driving the application only with this command line tool (each call is separate,
+the session is already running):
+
+ python3 {guitest}
+
+{help}
+
+Tips:
+- Start with `tree --showing --depth 3`, `windows` and `texts` to see what is on screen.
+- Use `info --name X` to see the roles and states of an element, e.g. the [checked] state.
+- Menu items: click the menu (e.g. --name File --role "menu item"), then click the entry.
+- Keyboard input goes to the focused element: `focus` first, then `type` / `key`.
+- The files of the test are in the test projects, they are already in place, use them as they are:{projects}
+ Do not make copies of them. In the script write {{project:NAME}} instead of the directory, it is another
+ directory every run. Anything else that you need can be created in {work} (use Write), do not touch other places.
+- A menu that is on screen but not in `tree`/`texts` (e.g. a context menu) can be read with `ocr`; check it in the
+ script with {{"expect": "ocr", "contains": "..."}} and use the keyboard (`key Down`, `key Return`) to choose in it.
+- Never read or modify the application source code.
+
+Test name: {name}
+
+The test is written in Gherkin: the Background and the Given steps are the starting point, the When steps are
+what you do and the Then steps are the EXPECTED result.
+
+{body}
+
+When you have done the test and checked the EXPECTED result (the Then steps), answer with a script that does the
+same without you. The script is replayed by a program, so:
+- use the exact roles and names that you saw. Give a name and a role. Use "path" only when nothing else tells
+ the element apart, paths change when the dialogs change (a file dialog's name field is found by its label)
+- a name matches any element that contains it, so always give the role too: a toolbar button
+ "Preferences" and the dialog "Preferences" have the same name, the dialog is role "dialog"
+- give every step a "label" that says what it is for, in the words of the test
+- end with "expect" steps that check the EXPECTED result, at least one for each Then step
+- the files of the test are the test projects, refer to them with {{project:NAME}} paths; files that
+ you made yourself are created by "write" steps (in {{work}}), not by you
+- the application is started fresh before the script, with an empty HOME
+- {matrixnote}
+- {editionnote}
+
+{scriptdoc}
+
+{previous}Check the EXPECTED result honestly. If the application does not do what EXPECTED says that is a finding, not
+a mistake in the script: still write the script that checks EXPECTED (it will fail, that is the point) and put
+the line `OBSERVED: FAIL - ` before the JSON block and add "observed_fail": true to
+the one check that fails. Otherwise put `OBSERVED: PASS`.
+
+Finish with the script as one JSON list in a ```json code block and nothing after it.
+If the test cannot be done through the GUI answer with the single line CANNOT: .
+"""
+
+
+PREVIOUS = """An earlier attempt gave this script:
+{script}
+
+When it was replayed from scratch by the program it failed:
+{error}
+
+Find out what is wrong (look at the GUI again if needed) and answer with a corrected script.
+
+"""
+
+
+def start_session(app, root, projects):
+ work = tempfile.mkdtemp(prefix='guitest-work-')
+ state = os.path.join(work, 'state.json')
+ env = dict(os.environ, GUITEST_STATE=state)
+ sub = lambda *a, **kw: subprocess.run([sys.executable, GUITEST, *a], env=env, text=True, **kw)
+ variables = testfile.materialize(root, projects, work)
+ r = sub('start', app, capture_output=True)
+ if r.returncode:
+ sys.exit('error: could not start: ' + (r.stdout + r.stderr).strip())
+ with open(state) as f:
+ home = json.load(f)['env']['HOME']
+ return work, home, env, sub, variables
+
+
+def explore(app, root, test, combo, edition, model, background, previous=''):
+ """Claude does the test by hand and returns (script, error); with a script the error is (OBSERVED, reason)."""
+ work, home, env, sub, variables = start_session(app, root, test.get('projects', []))
+ try:
+ note = ('this is a Scenario Outline, you explore it with the Examples row %s. Write {%s} in the script wherever '
+ 'that value is used, the script is run once for every row of the Examples' % (
+ ', '.join('%s=%s' % kv for kv in combo.items()), '}, {'.join(combo))
+ ) if combo else 'there are no Examples, write the values in the script as they are'
+ others = [e for e in testfile.editions(test) if e != edition]
+ if not edition:
+ editionnote = 'there is one edition of the application'
+ elif others:
+ editionnote = ('the application is the %s edition. The same script is also run with the %s edition, '
+ 'write {edition} where the name of the edition is shown' % (edition, ' and '.join(others)))
+ else:
+ editionnote = 'the script is only run with the %s edition' % edition
+ projects = ''.join('\n {project:%s} = %s, files: %s' % (n, variables['project:' + n],
+ ', '.join(sorted(testfile.project_files(root, n))))
+ for n in test.get('projects', [])) or ' none'
+ prompt = PROMPT.format(guitest=GUITEST, help=sub('--help', capture_output=True).stdout, work=work,
+ name=test['name'], body=testfile.body(test, combo, edition, background), matrixnote=note,
+ editionnote=editionnote,
+ scriptdoc=testfile.SCRIPT_DOC, previous=previous, projects=projects)
+ cmd = ['claude', '-p', '--allowedTools', 'Bash(python3 %s:*)' % GUITEST, 'Read', 'Write', '--add-dir', work]
+ if model:
+ cmd += ['--model', model]
+ p = subprocess.run(cmd, input=prompt, env=env, text=True, capture_output=True, cwd=work)
+ finally:
+ sub('stop', capture_output=True)
+ m = re.findall(r'^CANNOT:\s*(.*)$', p.stdout, re.M)
+ if m:
+ return None, 'Claude: cannot be tested through the GUI: ' + m[-1]
+ m = re.findall(r'```json\s*(.*?)```', p.stdout, re.S)
+ if not m:
+ return None, 'no script in the answer of Claude: ' + (p.stdout.strip() or p.stderr.strip())[:300]
+ try:
+ script = json.loads(m[-1])
+ except ValueError as e:
+ return None, 'the script is not valid JSON: %s' % e
+ errors = testfile.validate_script(script)
+ observed = re.findall(r'^OBSERVED:\s*(PASS|FAIL)\s*-?\s*(.*)$', p.stdout, re.M)
+ return (None, '; '.join(errors)) if errors else (script, observed[-1] if observed else ('PASS', ''))
+
+
+def replay(app, root, test, script):
+ work, home, env, sub, variables = start_session(app, root, test.get('projects', []))
+ try:
+ ok, msg, log = runner.run_script(sub, script, work, home, variables)
+ finally:
+ sub('stop', capture_output=True)
+ return ok, msg
+
+
+def store(path, test_id, script, made_for):
+ """Put the script in the file as it is now, the file may have been saved by the web interface meanwhile."""
+ doc = testfile.ensure_ids(testfile.load(path))
+ for t in doc['tests']:
+ if t['id'] == test_id:
+ t['script'], t['script_for'] = script, made_for
+ testfile.save(path, doc)
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument('app')
+ ap.add_argument('file')
+ ap.add_argument('--edition', choices=testfile.EDITIONS, help='the edition that APP is, only the tests for it')
+ ap.add_argument('--test', help='only tests whose name contains this')
+ ap.add_argument('--id', help='only the test with this id')
+ ap.add_argument('--force', action='store_true', help='also tests that have a script')
+ ap.add_argument('--model')
+ ap.add_argument('--attempts', type=int, default=3, help='tries per test, a failed replay is told to Claude')
+ args = ap.parse_args()
+ signal.signal(signal.SIGINT, signal.default_int_handler) # also when started in the background
+ if not args.file.endswith(testfile.EXT):
+ sys.exit('error: FILE must be a %s test file, convert markdown or JSON with testfile.py convert' % testfile.EXT)
+ app = os.path.abspath(args.app)
+ doc = testfile.ensure_ids(testfile.load(args.file))
+ root = os.path.dirname(os.path.abspath(args.file))
+ errors = testfile.validate(doc, root)
+ if errors:
+ sys.exit('error: ' + '; '.join(errors))
+
+ failed = 0
+ for t in doc['tests']:
+ if args.test and args.test.lower() not in t['name'].lower() or args.id and t['id'] != args.id:
+ continue
+ if (t.get('script') and not args.force) or not t.get('enabled', True):
+ continue
+ if args.edition and args.edition not in testfile.editions(t):
+ print('%s: not a test for the %s edition' % (t['name'], args.edition), flush=True)
+ continue
+ combo = testfile.combinations(t)[0]
+ values = dict(combo, edition=args.edition) if args.edition else combo
+ made_for = testfile.definition_hash(t, root, doc.get('background')) # the test may be edited while Claude works
+ print('%s: exploring...' % t['name'], flush=True)
+ previous, saved = '', False
+ for attempt in range(1, args.attempts + 1):
+ script, err = explore(app, root, t, combo, args.edition, args.model, doc.get('background'), previous)
+ if script is not None:
+ # the replay uses the first combination, as the exploration did
+ observed, why = err
+ ok, err = replay(app, root, t, testfile.subst_deep(script, values))
+ # a test that fails on the check that Claude saw fail by hand is a finding: the script is right
+ m = re.match(r'step (\d+)/', err)
+ finding = (not ok and observed == 'FAIL' and m and script[int(m.group(1)) - 1].get('observed_fail'))
+ if ok or finding:
+ store(args.file, t['id'], script, made_for)
+ print('%s: saved a script with %d steps (attempt %d)%s' % (
+ t['name'], len(script), attempt,
+ '. It FAILS on a check, as Claude saw: ' + why if finding else ''), flush=True)
+ saved = True
+ break
+ err = 'the replay of the script failed:\n' + err
+ previous = PREVIOUS.format(script=json.dumps(script, indent=1), error=err)
+ print('%s: attempt %d failed. %s' % (t['name'], attempt, err), flush=True)
+ if re.search(r'usage limit|rate limit|limit reached|out of credits', str(err), re.I):
+ sys.exit('error: Claude refuses to answer (limit), stopping. Run gen.py again later.')
+ if err.startswith('Claude: cannot'):
+ break
+ if not saved:
+ failed += 1
+ print('%s: no script saved' % t['name'], flush=True)
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/gui/manualtest/guitest.py b/gui/manualtest/guitest.py
new file mode 100755
index 00000000000..881d9f27bc5
--- /dev/null
+++ b/gui/manualtest/guitest.py
@@ -0,0 +1,708 @@
+#!/usr/bin/env python3
+"""GUI test helper for Qt applications, driven through AT-SPI.
+
+The application runs in its own Xvfb display with its own D-Bus and
+accessibility bus, so the real desktop is never touched. Each subcommand is a
+separate process; the session state (environment, pids) lives in a JSON file.
+
+ guitest.py start APP [ARGS...] start the session and the application
+ guitest.py tree show the accessibility tree
+ guitest.py click --name Recheck click an element
+ guitest.py stop kill everything
+
+State file: $GUITEST_STATE, default /tmp/guitest-/state.json
+"""
+
+import argparse
+import contextlib
+import io
+import glob
+import json
+import os
+import re
+import shutil
+import signal
+import subprocess
+import struct
+import sys
+import tempfile
+import threading
+import time
+import warnings
+
+warnings.filterwarnings("ignore")
+
+STATE = os.environ.get('GUITEST_STATE', '/tmp/guitest-%d/state.json' % os.getuid())
+
+
+# ---------------------------------------------------------------- session
+
+def load_state():
+ try:
+ with open(STATE) as f:
+ return json.load(f)
+ except FileNotFoundError:
+ sys.exit('error: no session, run "guitest.py start APP" first')
+
+
+# the shell tells how the application ended in app.log: 139 is SIGSEGV, 134 SIGABRT, 143 killed by us
+WRAPPER = '"$@" & echo $! > "$0"; wait $!; echo "application exited with status $?" >&2'
+
+
+def launch(cmd, env, tmp, log):
+ """Start the application. Returns the shell that waits for it and the pid of the application."""
+ pidfile = os.path.join(tmp, 'app.pid')
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+ p = subprocess.Popen(['sh', '-c', WRAPPER, pidfile] + cmd, env=env, start_new_session=True,
+ stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT)
+ for _ in range(100):
+ try:
+ with open(pidfile) as f:
+ return p, int(f.read())
+ except (OSError, ValueError):
+ time.sleep(0.05)
+ sys.exit('error: the application did not start')
+
+
+def exit_info(state):
+ """' (exited with status 139, SIGSEGV)' when the log says how the application ended, else ''."""
+ try:
+ with open(os.path.join(state['tmp'], 'app.log'), errors='replace') as f:
+ m = re.findall(r'application exited with status (\d+)', f.read())
+ except OSError:
+ return ''
+ if not m:
+ return ''
+ code = int(m[-1])
+ sig = ', ' + signal.Signals(code - 128).name if 128 < code < 160 else ''
+ return ' (exited with status %d%s)' % (code, sig)
+
+
+def start(args):
+ if os.path.exists(STATE):
+ sys.exit('error: session already running (%s), run stop first' % STATE)
+ app = os.path.abspath(args.app)
+ if not os.access(app, os.X_OK):
+ sys.exit('error: not executable: ' + app)
+ for tool in ('Xvfb', 'xdotool', 'dbus-daemon', '/usr/libexec/at-spi-bus-launcher'):
+ if not shutil.which(tool) and not os.path.exists(tool):
+ sys.exit('error: missing ' + tool)
+
+ tmp = tempfile.mkdtemp(prefix='guitest-')
+ runtime = os.path.join(tmp, 'run')
+ home = args.home or os.path.join(tmp, 'home')
+ os.makedirs(runtime, mode=0o700)
+ os.makedirs(home, exist_ok=True)
+ pids = []
+
+ def spawn(cmd, env, **kw):
+ kw.setdefault('stdout', subprocess.DEVNULL)
+ kw.setdefault('stderr', subprocess.DEVNULL)
+ p = subprocess.Popen(cmd, env=env, start_new_session=True, stdin=subprocess.DEVNULL, **kw)
+ pids.append(p.pid)
+ return p
+
+ env = dict(os.environ)
+ env.update(XDG_RUNTIME_DIR=runtime, HOME=home)
+ for k in ('XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_CONFIG_DIRS'):
+ env.pop(k, None) # QSettings must not find the real preferences
+ env.pop('WAYLAND_DISPLAY', None)
+ env.pop('DBUS_SESSION_BUS_ADDRESS', None)
+
+ # session bus
+ out = subprocess.run(['dbus-daemon', '--session', '--fork', '--print-address', '--print-pid'],
+ env=env, capture_output=True, text=True, check=True).stdout.split()
+ env['DBUS_SESSION_BUS_ADDRESS'] = out[0]
+ pids.append(int(out[1]))
+
+ # X server
+ r, w = os.pipe()
+ spawn(['Xvfb', '-displayfd', str(w), '-screen', '0', args.size + 'x24', '-fbdir', tmp],
+ env, pass_fds=(w,))
+ os.close(w)
+ env['DISPLAY'] = ':' + os.read(r, 16).decode().strip()
+ os.close(r)
+
+ # accessibility bus
+ spawn(['/usr/libexec/at-spi-bus-launcher', '--launch-immediately'], env)
+ for _ in range(100):
+ if glob.glob(os.path.join(runtime, 'at-spi', 'bus_*')):
+ break
+ time.sleep(0.1)
+ else:
+ kill(pids, tmp)
+ sys.exit('error: accessibility bus did not start')
+
+ env.update(QT_QPA_PLATFORM='xcb', QT_ACCESSIBILITY='1',
+ QT_LINUX_ACCESSIBILITY_ALWAYS_ON='1', QT_NO_XDG_DESKTOP_PORTAL='1',
+ QT_QPA_PLATFORMTHEME='generic', # not the KDE file dialog, its KIO jobs crash in this session
+ LANG='C', LC_ALL='C')
+ log = open(os.path.join(tmp, 'app.log'), 'w')
+ appproc, app_pid = launch([app] + args.args, env, tmp, log)
+ pids.append(appproc.pid)
+
+ state = dict(env={k: env[k] for k in ('XDG_RUNTIME_DIR', 'HOME', 'DBUS_SESSION_BUS_ADDRESS', 'DISPLAY',
+ 'QT_QPA_PLATFORM', 'QT_ACCESSIBILITY',
+ 'QT_LINUX_ACCESSIBILITY_ALWAYS_ON', 'QT_NO_XDG_DESKTOP_PORTAL', 'QT_QPA_PLATFORMTHEME',
+ 'LANG', 'LC_ALL')},
+ pids=pids, app_pid=app_pid, app_pgid=appproc.pid, app_cmd=[app] + args.args, tmp=tmp)
+ os.makedirs(os.path.dirname(STATE), exist_ok=True)
+ with open(STATE, 'w') as f:
+ json.dump(state, f)
+
+ # wait for the application to show up on the accessibility bus
+ os.environ.update(state['env'])
+ for _ in range(100):
+ if appproc.poll() is not None:
+ print(open(os.path.join(tmp, 'app.log')).read())
+ kill(pids, tmp)
+ os.remove(STATE)
+ sys.exit('error: application exited with code %s' % appproc.returncode)
+ if find_app(state) is not None:
+ print('started, display %s, app log %s/app.log' % (env['DISPLAY'], tmp))
+ return
+ time.sleep(0.2)
+ sys.exit('error: application did not appear on the accessibility bus (see %s/app.log)' % tmp)
+
+
+def restart(args):
+ """Quit the application and start it again, same HOME so settings are kept."""
+ state = load_state()
+ try:
+ os.killpg(state['app_pgid'], signal.SIGTERM)
+ except ProcessLookupError:
+ pass
+ time.sleep(1)
+ env = dict(os.environ)
+ env.update(state['env'])
+ log = open(os.path.join(state['tmp'], 'app.log'), 'a')
+ p, state['app_pid'] = launch(state['app_cmd'], env, state['tmp'], log)
+ state['app_pgid'] = p.pid
+ state['pids'].append(p.pid)
+ with open(STATE, 'w') as f:
+ json.dump(state, f)
+ for _ in range(100):
+ if find_app(state) is not None:
+ print('restarted')
+ return
+ time.sleep(0.2)
+ sys.exit('error: application did not come back')
+
+
+def kill(pids, tmp):
+ for pid in reversed(pids):
+ try:
+ os.killpg(pid, signal.SIGTERM)
+ except (ProcessLookupError, PermissionError):
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except (ProcessLookupError, PermissionError):
+ pass
+ time.sleep(0.3)
+ shutil.rmtree(tmp, ignore_errors=True)
+
+
+def stop(args):
+ try:
+ state = load_state()
+ except SystemExit:
+ return
+ kill(state['pids'], state['tmp'])
+ os.remove(STATE)
+ print('stopped')
+
+
+# ---------------------------------------------------------------- AT-SPI
+
+_atspi = None
+
+
+def atspi():
+ global _atspi
+ if _atspi is None:
+ os.environ.update(load_state()['env'])
+ import gi
+ gi.require_version('Atspi', '2.0')
+ from gi.repository import Atspi
+ _atspi = Atspi
+ return _atspi
+
+
+def find_app(state):
+ A = atspi()
+ desktop = A.get_desktop(0)
+ for i in range(desktop.get_child_count()):
+ try:
+ app = desktop.get_child_at_index(i)
+ if app is not None and app.get_process_id() == state['app_pid']:
+ return app
+ except Exception:
+ pass
+ return None
+
+
+def app_running(state):
+ """The process is there (a zombie is a crashed application nobody has collected)."""
+ try:
+ with open('/proc/%d/stat' % state['app_pid']) as f:
+ return f.read().rsplit(')', 1)[1].split()[0] != 'Z'
+ except OSError:
+ return False
+
+
+def get_app():
+ state = load_state()
+ for _ in range(25): # the application does not answer on the accessibility bus while a modal dialog opens
+ app = find_app(state)
+ if app is not None:
+ return app
+ if not app_running(state):
+ break
+ time.sleep(0.2)
+ if app_running(state):
+ sys.exit('error: the application is running but does not answer on the accessibility bus (busy or hung?)')
+ sys.exit('error: the application is not running%s. log: %s/app.log' % (exit_info(state) or ' (crashed?)', state['tmp']))
+
+
+def children(node):
+ try:
+ for i in range(node.get_child_count()):
+ c = node.get_child_at_index(i)
+ if c is not None:
+ yield i, c
+ except Exception:
+ return
+
+
+def states(node):
+ A = atspi()
+ st = node.get_state_set()
+ names = []
+ for label, s in (('showing', A.StateType.SHOWING), ('enabled', A.StateType.ENABLED),
+ ('focused', A.StateType.FOCUSED), ('checked', A.StateType.CHECKED),
+ ('selected', A.StateType.SELECTED), ('expanded', A.StateType.EXPANDED),
+ ('editable', A.StateType.EDITABLE), ('modal', A.StateType.MODAL)):
+ if st.contains(s):
+ names.append(label)
+ return names
+
+
+def walk(node, path=(), depth=0):
+ yield path, node, depth
+ for i, c in children(node):
+ yield from walk(c, path + (i,), depth + 1)
+
+
+def node_text(node):
+ # the interface functions take the accessible as first argument, node.get_text_iface() does not work here
+ A = atspi()
+ try:
+ if 'Text' in A.Accessible.get_interfaces(node):
+ return A.Text.get_text(node, 0, min(A.Text.get_character_count(node), 2000))
+ except Exception:
+ pass
+ return None
+
+
+def node_value(node):
+ A = atspi()
+ try:
+ if 'Value' in A.Accessible.get_interfaces(node):
+ return A.Value.get_current_value(node)
+ except Exception:
+ pass
+ return None
+
+
+def extents(node):
+ A = atspi()
+ try:
+ e = node.get_component_iface().get_extents(A.CoordType.SCREEN)
+ return e.x, e.y, e.width, e.height
+ except Exception:
+ return None
+
+
+def describe(path, node, with_geom=True):
+ name = node.get_name() or ''
+ s = '%s %s %r' % ('.'.join(map(str, path)) or '-', node.get_role_name(), name)
+ text = node_text(node)
+ if text and text != name:
+ s += ' text=%r' % (text if len(text) < 200 else text[:200] + '...')
+ val = node_value(node)
+ if val is not None:
+ s += ' value=%s' % val
+ st = states(node)
+ if st:
+ s += ' [%s]' % ','.join(st)
+ g = extents(node)
+ if with_geom and g and g[2] > 0:
+ s += ' @%d,%d %dx%d' % g
+ return s
+
+
+def cmd_tree(args):
+ app = get_app()
+ root, rootpath = app, ()
+ if args.path:
+ rootpath = tuple(int(x) for x in args.path.split('.'))
+ for i in rootpath:
+ root = root.get_child_at_index(i)
+ for path, node, depth in walk(root, rootpath):
+ d = len(path) - len(rootpath)
+ if d > args.depth:
+ continue
+ if args.showing and 'showing' not in states(node) and d > 0:
+ continue
+ print(' ' * d + describe(path, node))
+
+
+def cmd_windows(args):
+ app = get_app()
+ for i, c in children(app):
+ print(describe((i,), c))
+
+
+def cmd_texts(args):
+ """All visible text on screen, one line per element, for checking EXPECTED."""
+ app = get_app()
+ for path, node, depth in walk(app):
+ if 'showing' not in states(node):
+ continue
+ role = node.get_role_name()
+ name = node.get_name() or ''
+ text = node_text(node)
+ if role in ('label', 'push button', 'check box', 'radio button', 'menu item', 'text', 'entry',
+ 'page tab', 'combo box', 'table cell', 'tree item', 'list item', 'status bar',
+ 'title bar', 'spin button', 'frame', 'dialog', 'alert') or text:
+ line = '%s %s %r' % ('.'.join(map(str, path)), role, name)
+ if text and text != name:
+ line += ' text=%r' % text
+ if 'checked' in states(node):
+ line += ' [checked]'
+ print(line)
+
+
+# ---------------------------------------------------------------- finding
+
+def select(args, need_showing=True):
+ app = get_app()
+ if args.path:
+ node = app
+ for i in (int(x) for x in args.path.split('.')):
+ node = node.get_child_at_index(i)
+ if node is None:
+ sys.exit('error: no such path')
+ return node
+ matches = []
+ for path, node, depth in walk(app):
+ role = node.get_role_name()
+ if args.role and args.role.lower() != role.lower():
+ continue
+ name = node.get_name() or ''
+ text = '' # found by name only: the text of a panel would match the message of an error
+ if args.name is not None:
+ want = args.name.lower()
+ if args.exact:
+ if name.lower() != want and text.lower() != want:
+ continue
+ elif want not in name.lower() and want not in text.lower():
+ continue
+ elif not args.role:
+ sys.exit('error: give --name, --role or --path')
+ if need_showing and 'showing' not in states(node):
+ continue
+ matches.append((path, node))
+ if not matches:
+ sys.exit('error: no matching visible element (name=%r role=%r); try "tree --showing"' % (args.name, args.role))
+ if args.nth is None and len(matches) > 1:
+ lines = '\n'.join(' ' + describe(p, n) for p, n in matches[:15])
+ sys.exit('error: %d matches, narrow with --role/--exact or pick with --nth N (0-based):\n%s' % (len(matches), lines))
+ nth = args.nth or 0
+ if nth >= len(matches):
+ sys.exit('error: no matching visible element: only %d match(es) for name=%r role=%r, nth=%d is too high'
+ % (len(matches), args.name, args.role, nth))
+ return matches[nth][1]
+
+
+def xdo(*a):
+ env = dict(os.environ)
+ env.update(load_state()['env'])
+ subprocess.run(['xdotool'] + [str(x) for x in a], env=env, check=True)
+
+
+def center(node):
+ g = extents(node)
+ if not g or g[2] <= 0:
+ sys.exit('error: element has no geometry')
+ return g[0] + g[2] // 2, g[1] + g[3] // 2
+
+
+def mouse(node, button=1, count=1):
+ x, y = center(node)
+ xdo('mousemove', x, y)
+ time.sleep(0.05)
+ xdo('click', '--repeat', count, '--delay', 80, button)
+
+
+def do_action(node):
+ A = atspi()
+ try:
+ act = node.get_action_iface()
+ if act is None:
+ return False
+ for i in range(act.get_n_actions()):
+ if act.get_action_name(i).lower() in ('click', 'press', 'toggle', 'activate', 'check', 'uncheck'):
+ return act.do_action(i)
+ except Exception:
+ pass
+ return False
+
+
+def cmd_click(args):
+ node = select(args)
+ desc = describe((), node, False)
+ if args.mouse:
+ mouse(node)
+ else:
+ # the action of a button that opens a modal dialog does not return until the dialog is closed
+ done = []
+ t = threading.Thread(target=lambda: done.append(do_action(node)), daemon=True)
+ t.start()
+ t.join(args.settle + 1)
+ if done == [False]:
+ mouse(node)
+ elif not done:
+ time.sleep(args.settle)
+ print('clicked ' + desc, flush=True)
+ os._exit(0)
+ time.sleep(args.settle)
+ print('clicked ' + desc)
+
+
+def cmd_rightclick(args):
+ node = select(args)
+ mouse(node, 3)
+ time.sleep(args.settle)
+ print('right-clicked ' + describe((), node, False))
+
+
+def cmd_doubleclick(args):
+ node = select(args)
+ mouse(node, 1, 2)
+ time.sleep(args.settle)
+ print('double-clicked ' + describe((), node, False))
+
+
+def cmd_focus(args):
+ node = select(args)
+ ok = node.get_component_iface().grab_focus()
+ if not ok:
+ mouse(node)
+ print('focused ' + describe((), node, False))
+
+
+def cmd_settext(args):
+ node = select(args)
+ et = node.get_editable_text_iface()
+ if et is None:
+ sys.exit('error: element is not editable text')
+ et.set_text_contents(args.value)
+ time.sleep(args.settle)
+ print('set text of ' + describe((), node, False))
+
+
+def cmd_type(args):
+ xdo('type', '--delay', 20, '--', args.value)
+ time.sleep(args.settle)
+
+
+def cmd_key(args):
+ xdo('key', *args.keys)
+ time.sleep(args.settle)
+
+
+def cmd_hover(args):
+ x, y = center(select(args))
+ xdo('mousemove', x, y)
+
+
+def cmd_wait(args):
+ deadline = time.time() + args.timeout
+ while True:
+ try:
+ node = select(args)
+ found = describe((), node, False)
+ except SystemExit as e:
+ if 'matches' in str(e): # ambiguous means it exists
+ found = str(e).split('\n')[0]
+ else:
+ found = None
+ if args.gone != (found is not None):
+ print('gone' if args.gone else 'found ' + found)
+ return
+ if time.time() > deadline:
+ sys.exit('error: timeout waiting for element %s' % ('to disappear' if args.gone else 'to appear'))
+ time.sleep(0.3)
+
+
+def cmd_info(args):
+ """One line about the element: role, name, text, value and [states]."""
+ print(describe((), select(args), False))
+
+
+def cmd_suggest(args):
+ """Visible named elements, the ones most like --name first (for error messages)."""
+ import difflib
+ want = (args.name or '').lower()
+ items = []
+ for path, node, depth in walk(get_app()):
+ name = (node.get_name() or '').strip()
+ if name and 'showing' in states(node):
+ items.append((node.get_role_name(), name))
+ items.sort(key=lambda it: ((it[0].lower() != (args.role or '').lower()) if args.role else 0,
+ -difflib.SequenceMatcher(None, want, it[1].lower()).ratio()))
+ seen = []
+ for it in items:
+ if it not in seen:
+ seen.append(it)
+ print('%s %r' % it)
+ if len(seen) >= args.limit:
+ break
+
+
+def cmd_screenshot(args):
+ from PIL import Image
+ state = load_state()
+ src = os.path.join(state['tmp'], 'Xvfb_screen0')
+ data = open(src, 'rb').read()
+ (hdr, _ver, _fmt, _depth, w, h, _xoff, _bo, _bu, _bbo, _bp, bpp, bpl, _vc,
+ rm, gm, bm, _bpr, _ncm, ncolors) = struct.unpack('>20I', data[:80])
+ off = hdr + ncolors * 12
+ img = Image.frombuffer('RGB', (w, h), data[off:off + bpl * h], 'raw', 'BGRX', bpl, 1)
+ img.save(args.file)
+ print('saved %s (%dx%d)' % (args.file, w, h))
+
+
+def cmd_ocr(args):
+ """Text on the screen read by tesseract, also what AT-SPI does not show (some context menus)."""
+ import tempfile
+ png = os.path.join(tempfile.mkdtemp(prefix='guitest-ocr-'), 'screen.png')
+ args.file = png
+ with contextlib.redirect_stdout(io.StringIO()):
+ cmd_screenshot(args)
+ from PIL import Image
+ img = Image.open(png)
+ img = img.resize((img.width * 2, img.height * 2), Image.LANCZOS)
+ img.save(png)
+ p = subprocess.run(['tesseract', png, '-', '--psm', '11'], capture_output=True, text=True)
+ if p.returncode:
+ sys.exit('error: tesseract failed: ' + p.stderr.strip())
+ shutil.rmtree(os.path.dirname(png), ignore_errors=True)
+ print(p.stdout)
+
+
+def cmd_log(args):
+ print(open(os.path.join(load_state()['tmp'], 'app.log')).read())
+
+
+def cmd_alive(args):
+ sys.exit(0 if app_running(load_state()) else 'application is not running')
+
+
+# ---------------------------------------------------------------- cli
+
+def selector(p):
+ p.add_argument('--name', help='substring of accessible name or text (case-insensitive)')
+ p.add_argument('--role', help='role, e.g. "push button", "menu item", "tree item", "check box"')
+ p.add_argument('--path', help='path from "tree", e.g. 0.1.2')
+ p.add_argument('--nth', type=int, help='pick the Nth match (0-based)')
+ p.add_argument('--exact', action='store_true', help='--name must match exactly')
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ sub = ap.add_subparsers(dest='cmd', required=True)
+
+ p = sub.add_parser('start', help='start session and application')
+ p.add_argument('app')
+ p.add_argument('args', nargs='*')
+ p.add_argument('--home', help='HOME for the application (default: empty temporary dir)')
+ p.add_argument('--size', default='1280x900')
+ p.set_defaults(f=start)
+
+ sub.add_parser('stop', help='stop session').set_defaults(f=stop)
+ sub.add_parser('restart', help='quit and start the application again (settings are kept)').set_defaults(f=restart)
+ sub.add_parser('alive', help='exit 0 if the application is running').set_defaults(f=cmd_alive)
+ sub.add_parser('log', help='application stdout/stderr').set_defaults(f=cmd_log)
+ sub.add_parser('windows', help='top level windows and dialogs').set_defaults(f=cmd_windows)
+ sub.add_parser('texts', help='all visible texts').set_defaults(f=cmd_texts)
+
+ p = sub.add_parser('tree', help='accessibility tree')
+ p.add_argument('--depth', type=int, default=99)
+ p.add_argument('--path', help='only this subtree')
+ p.add_argument('--showing', action='store_true', help='only visible elements')
+ p.set_defaults(f=cmd_tree)
+
+ for name, f, h in (('click', cmd_click, 'click (uses the accessibility action when there is one)'),
+ ('rightclick', cmd_rightclick, 'right click (opens context menu)'),
+ ('doubleclick', cmd_doubleclick, 'double click'),
+ ('focus', cmd_focus, 'give keyboard focus'),
+ ('hover', cmd_hover, 'move the mouse over the element')):
+ p = sub.add_parser(name, help=h)
+ selector(p)
+ p.add_argument('--settle', type=float, default=0.5, help='seconds to wait afterwards')
+ if name == 'click':
+ p.add_argument('--mouse', action='store_true', help='use a real mouse click')
+ p.set_defaults(f=f)
+
+ p = sub.add_parser('settext', help='replace the text of an edit field')
+ selector(p)
+ p.add_argument('value')
+ p.add_argument('--settle', type=float, default=0.3)
+ p.set_defaults(f=cmd_settext)
+
+ p = sub.add_parser('type', help='type text with the keyboard into the focused element')
+ p.add_argument('value')
+ p.add_argument('--settle', type=float, default=0.3)
+ p.set_defaults(f=cmd_type)
+
+ p = sub.add_parser('key', help='press keys, xdotool syntax, e.g. Return ctrl+o Escape')
+ p.add_argument('keys', nargs='+')
+ p.add_argument('--settle', type=float, default=0.3)
+ p.set_defaults(f=cmd_key)
+
+ p = sub.add_parser('wait', help='wait until a (visible) element exists')
+ selector(p)
+ p.add_argument('--timeout', type=float, default=10)
+ p.add_argument('--gone', action='store_true', help='wait until it disappears')
+ p.set_defaults(f=cmd_wait)
+
+ p = sub.add_parser('info', help='role, name, text, value and states of one element')
+ selector(p)
+ p.set_defaults(f=cmd_info)
+
+ p = sub.add_parser('suggest', help='visible elements most similar to --name/--role')
+ p.add_argument('--name')
+ p.add_argument('--role')
+ p.add_argument('--limit', type=int, default=8)
+ p.set_defaults(f=cmd_suggest)
+
+ p = sub.add_parser('ocr', help='print the text on the screen as read by tesseract (for what "texts" does not show)')
+ p.set_defaults(f=cmd_ocr)
+ p = sub.add_parser('screenshot', help='save a PNG of the screen')
+ p.add_argument('file')
+ p.set_defaults(f=cmd_screenshot)
+
+ args = ap.parse_args()
+ try: # command history next to the state file, so a watcher can see what a test is doing
+ with open(os.path.join(os.path.dirname(STATE), 'commands.log'), 'a') as f:
+ f.write(time.strftime('%H:%M:%S ') + ' '.join(sys.argv[1:]) + '\n')
+ except OSError:
+ pass
+ args.f(args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/gui/manualtest/mainwindow.md b/gui/manualtest/mainwindow.md
deleted file mode 100644
index 563a538530f..00000000000
--- a/gui/manualtest/mainwindow.md
+++ /dev/null
@@ -1,21 +0,0 @@
-
-# Main window
-
-Some manual testing in the main window interface
-
-
-## Test: Recheck file
-
-1. Load a project with results.
-1. Restart the GUI.
-1. Right click on a file and click on "Recheck".
-
-EXPECTED: Results for that file should be refreshed.
-
-
-## Test: Switch project
-
-1. Open project #1
-1. Open project #2 that has different results
-
-EXPECTED: Results for project #1 should go away, results for project #2 should be shown
diff --git a/gui/manualtest/projectfiledialog.md b/gui/manualtest/projectfiledialog.md
deleted file mode 100644
index ce7d39b1df0..00000000000
--- a/gui/manualtest/projectfiledialog.md
+++ /dev/null
@@ -1,52 +0,0 @@
-
-# Project file dialog
-
-Some manual testing in the project file dialog interface
-
-
-## Test: Relative paths
-
-Ticket: #14983
-
-1. Configure files/paths in project folder:
- * import a projectfile
- * add include paths in project folder
- * exclude file/folder
-
-2. Save project
-
-EXPECTED: Relative paths should be used in the XML
-
-
-## Test: Platform file pic8.xml
-
-Ticket: #14489
-
-EXPECTED: In the project file dialog it should be possible to select xml files i.e. pic8.xml
-
-TODO: can this test be automated
-
-
-## Test: Custom cfg file
-
-Ticket: #14672
-
-1. Copy addons/avr.cfg to a file "aa.cfg" in same folder as a cppcheck GUI project file
-
-EXPECTED: It should not be possible to activate "aa.cfg" in the project file dialog, it should appear in alphabetical order.
-
-
-## Test: Misra C checkbox
-
-Ticket: #14488
-
-Matrix: Use both open source and premium
-
-1. Load project file in trac ticket 14488
-1. Goto "Edit project"
-1. Goto "Addons" tab
-
-EXPECTED: The misra c checkbox should be checked
-
-TODO: can this test be automated
-
diff --git a/gui/manualtest/report.py b/gui/manualtest/report.py
new file mode 100644
index 00000000000..e84df758eb9
--- /dev/null
+++ b/gui/manualtest/report.py
@@ -0,0 +1,408 @@
+"""The HTML report of a run.py run: summary, results per test file, history over the earlier runs,
+the failed tests with their screenshots and all tests. One file, no external resources.
+
+write(path, run, history) where run is what run.py saves as results.json and history is the
+results.json of earlier runs of the same edition, oldest first.
+"""
+
+import html
+import os
+
+STATUSES = ('PASS', 'FAIL', 'SKIP')
+ICON = {'PASS': '✓', 'FAIL': '✗', 'SKIP': '–', None: ''}
+COLOR = {'PASS': 'var(--pass)', 'FAIL': 'var(--fail)', 'SKIP': 'var(--skip)'}
+HISTORY_RUNS = 15 # columns in the history, including this run
+
+CSS = """
+:root {
+ --ink: #1b2330; --ink-2: #4a5568; --muted: #6b7686; --line: #e2e6ec; --line-2: #cfd6df; --paper: #fff; --band: #f7f8fa;
+ --accent: #2456c8; --accent-soft: #e8eefb;
+ --pass: #0ca30c; --pass-bg: #e4f5e4; --pass-ink: #0b5a0b;
+ --fail: #d03b3b; --fail-bg: #fbe6e6; --fail-ink: #9a1d1d;
+ --skip: #fab219; --skip-bg: #fdf2d6; --skip-ink: #6e4b00;
+ --none: #dfe3e9;
+ --sans: system-ui, -apple-system, "Segoe UI", Ubuntu, Cantarell, Roboto, sans-serif;
+ --mono: ui-monospace, "DejaVu Sans Mono", Menlo, Consolas, monospace;
+ color-scheme: light;
+}
+* { box-sizing: border-box; }
+body { margin: 0; background: var(--paper); color: var(--ink); font: 14px/1.5 var(--sans); }
+main { max-width: 72rem; margin: 0 auto; padding: 1.6rem max(16px, 2.2rem) 3rem; }
+h1, h2, h3 { margin: 0; text-wrap: balance; }
+a { color: var(--accent); }
+section { margin-top: 2rem; }
+h2 { font-size: 1.02rem; font-weight: 650; margin-bottom: .2rem; }
+.sub { color: var(--muted); font-size: .85rem; margin: 0 0 .8rem; }
+.rhead { display: flex; gap: 1rem 2rem; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
+.rhead h1 { font-size: 1.35rem; font-weight: 650; }
+.eyebrow { text-transform: uppercase; letter-spacing: .08em; font-size: .72rem; color: var(--muted); font-weight: 600; }
+.verdict { border-radius: 8px; padding: .45rem .85rem; font-weight: 650; }
+.verdict.FAIL { background: var(--fail-bg); color: var(--fail-ink); }
+.verdict.PASS { background: var(--pass-bg); color: var(--pass-ink); }
+.meta { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: .55rem 1.5rem; margin: 1.1rem 0 0;
+ padding-top: 1rem; border-top: 1px solid var(--line); }
+.meta div { min-width: 0; }
+.meta dt { font-size: .75rem; color: var(--muted); }
+.meta dd { margin: 0; overflow-wrap: anywhere; }
+.meta dd.mono { font-family: var(--mono); font-size: .85rem; }
+.dirty, .aborted { color: var(--fail-ink); }
+.dirty { font-size: .75rem; margin-left: .3rem; }
+.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: .8rem; margin-top: 1.4rem; }
+.tile { border: 1px solid var(--line); border-radius: 8px; padding: .75rem .9rem; }
+.tile .lbl { font-size: .8rem; color: var(--muted); display: flex; align-items: center; gap: .4rem; }
+.tile .val { font-size: 1.7rem; font-weight: 650; line-height: 1.2; font-variant-numeric: tabular-nums; }
+.tile .note { font-size: .78rem; color: var(--muted); }
+.dot { width: .6rem; height: .6rem; border-radius: 50%; display: inline-block; flex: none; }
+.d-PASS { background: var(--pass); } .d-FAIL { background: var(--fail); } .d-SKIP { background: var(--skip); }
+.chip { display: inline-block; border-radius: 5px; padding: 0 .45rem; font-size: .75rem; font-weight: 650; line-height: 1.6; white-space: nowrap; }
+.c-PASS { background: var(--pass-bg); color: var(--pass-ink); }
+.c-FAIL { background: var(--fail-bg); color: var(--fail-ink); }
+.c-SKIP { background: var(--skip-bg); color: var(--skip-ink); }
+.c-CRASH { background: var(--fail-ink); color: #fff; }
+.c-NEW { background: var(--accent-soft); color: var(--accent); }
+.legend { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .8rem; color: var(--ink-2); margin: .2rem 0 .5rem; }
+.legend span { display: inline-flex; align-items: center; gap: .35rem; }
+.chart { overflow-x: auto; }
+.chart svg { display: block; width: 100%; height: auto; min-width: 34rem; }
+.chart text { font-family: var(--sans); fill: var(--ink-2); font-size: 12px; }
+.chart text.m { fill: var(--muted); font-size: 11px; }
+.chart text.b { fill: var(--ink); font-weight: 600; }
+.chart text.sha { font-family: var(--mono); font-size: 9.5px; fill: var(--muted); }
+.chart .grid { stroke: var(--line); stroke-width: 1; }
+.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1.5rem 2.5rem; }
+.cols > * { min-width: 0; }
+.changes { display: grid; gap: .45rem; }
+.changes div { display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap; }
+.file { color: var(--muted); font-size: .8rem; }
+.tbl { overflow-x: auto; }
+table.tests { border-collapse: collapse; width: 100%; min-width: 40rem; }
+.tests th { text-align: left; font-size: .75rem; color: var(--muted); font-weight: 600; padding: .35rem .6rem; border-bottom: 1px solid var(--line-2); }
+.tests td { padding: .45rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
+.tests tr.grp td { background: var(--band); font-weight: 600; font-size: .82rem; color: var(--ink-2); padding-top: .55rem; }
+.tests tr.grp span { font-weight: 400; color: var(--muted); }
+.tests td.t { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-2); }
+.tests td.links { white-space: nowrap; font-size: .82rem; }
+.tests details { margin-top: .3rem; font-size: .82rem; }
+.tests summary { color: var(--muted); cursor: pointer; }
+.reason { font-family: var(--mono); font-size: .8rem; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: anywhere; }
+.strip { display: flex; gap: 2px; width: 11rem; margin-top: .35rem; }
+.strip i { flex: 1; height: .5rem; background: var(--pass); }
+.strip i:first-child { border-radius: 3px 0 0 3px; } .strip i:last-child { border-radius: 0 3px 3px 0; }
+.strip i.f { background: var(--fail); } .strip i.n { background: var(--none); }
+.stripnote { font-size: .75rem; color: var(--muted); }
+.card { border: 1px solid var(--line); border-left: 4px solid var(--fail); border-radius: 8px; padding: .9rem 1rem; display: grid; gap: .5rem; }
+.card + .card { margin-top: .8rem; }
+.card.SKIP { border-left-color: var(--skip); }
+.card-head { display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap; }
+.card-head h3 { font-size: .98rem; }
+.card-head .links { margin-left: auto; font-size: .82rem; }
+.film { display: flex; gap: .6rem; overflow-x: auto; padding-bottom: .3rem; }
+.shot { flex: none; width: 10.5rem; font-size: .75rem; color: var(--ink-2); text-decoration: none; }
+.shot:hover b { text-decoration: underline; }
+.shot img { display: block; width: 100%; aspect-ratio: 4/3; object-fit: cover; object-position: left top;
+ border: 1px solid var(--line-2); border-radius: 5px; background: var(--band); }
+.shot.bad img { outline: 3px solid var(--fail); outline-offset: -1px; }
+.shot b { color: var(--ink); }
+.film.small .shot { width: 7.5rem; }
+table.hgrid { border-collapse: separate; border-spacing: 3px; }
+.hgrid th.name { text-align: right; padding-right: .6rem; font-size: .82rem; font-weight: 500; white-space: nowrap; }
+.hgrid th.run { font-size: .68rem; color: var(--muted); font-weight: 500; text-align: center; }
+.hgrid th.run.cur { color: var(--ink); font-weight: 700; }
+.hgrid td.cell { width: 1.6rem; height: 1.35rem; border-radius: 3px; text-align: center; font-size: .72rem; font-weight: 700; color: #fff; background: var(--band); }
+.hgrid td.PASS { background: var(--pass); } .hgrid td.FAIL { background: var(--fail); }
+.hgrid td.SKIP { background: var(--skip); color: var(--skip-ink); }
+.hgrid td.cur { box-shadow: 0 0 0 2px var(--paper), 0 0 0 3.5px var(--ink); }
+.hgrid td.note { font-size: .78rem; color: var(--muted); padding-left: .7rem; white-space: nowrap; }
+@media (max-width: 520px) { .card-head .links { margin-left: 0; width: 100%; } }
+"""
+
+e = html.escape
+
+
+def dur(s):
+ s = int(round(s))
+ return '%d min %d s' % (s // 60, s % 60) if s >= 60 else '%d s' % s
+
+
+def chip(status):
+ return '%s %s' % (status, ICON[status], status)
+
+
+def crash_chip(r):
+ return ' CRASH' if r['crash'] else ''
+
+
+def count(results, status):
+ return sum(1 for r in results if r['status'] == status)
+
+
+def short_date(started):
+ return '%d/%d' % (int(started[5:7]), int(started[8:10]))
+
+
+def links(r, outdir):
+ """Links to the files of a test that exist (a skipped test has none)."""
+ out = [(r['slug'] + ext, text) for ext, text in (('.txt', 'steps'), ('.app.log', 'app log'), ('.png', 'screenshot'))
+ if os.path.exists(os.path.join(outdir, r['slug'] + ext))]
+ return ' · '.join('%s' % (e(f), t) for f, t in out)
+
+
+def strip(r):
+ """One cell per step: green passed, red failed, grey not run."""
+ n, at = r.get('steps', 0), r.get('failed_step', 0)
+ if not n:
+ return ''
+ if r['status'] == 'SKIP':
+ cells, note = ['n'] * n, 'not run'
+ elif r['status'] == 'FAIL' and not at and not r['crash']:
+ cells, note = ['n'] * n, 'did not start'
+ else:
+ cells = ['' if not at or k < at else 'f' if k == at else 'n' for k in range(1, n + 1)]
+ note = 'failed at step %d of %d' % (at, n) if at else '%d of %d steps' % (n, n)
+ return '
%s
%s
' % (
+ note, ''.join('' % c for c in cells), note)
+
+
+def hbar(x, y, w, h, r, fill, tip):
+ """A horizontal bar, rounded at the data end only."""
+ r = min(r, w / 2, h / 2)
+ return ('%s'
+ % (x, y, w - r, r, r, r, r, h - 2 * r, r, r, r, r, w - r, fill, e(tip)))
+
+
+def vbar(x, base, w, h, r, fill, tip, top):
+ """A column segment; the top segment is rounded at the data end."""
+ if not top:
+ return '%s' % (x, base - h, w, h, fill, e(tip))
+ r = min(r, w / 2, h)
+ return ('%s'
+ % (x, base, h - r, r, r, r, r, w - 2 * r, r, r, r, r, h - r, fill, e(tip)))
+
+
+def legend():
+ return '
%s
' % ''.join('%s' % (k, k) for k in STATUSES)
+
+
+def per_file_chart(run):
+ rows = []
+ for f in run['files']:
+ rs = [r for r in run['results'] if r['file'] == f['file']]
+ if rs:
+ rows.append(dict({k: count(rs, k) for k in STATUSES}, title=f['title'] or f['file'], n=len(rs)))
+ big = max(r['n'] for r in rows)
+ step = 1 if big <= 10 else 5 if big <= 50 else 20
+ top = -(-big // step) * step
+ left, width, rh, h = 210, 900, 30, 18
+ bw = width - left - 70
+ sx = lambda v: v / top * bw
+ s = []
+ for v in range(0, top + 1, step):
+ s.append('%d'
+ % (left + sx(v), left + sx(v), len(rows) * rh + 4, left + sx(v), len(rows) * rh + 20, v))
+ for i, r in enumerate(rows):
+ y = 8 + i * rh
+ title = r['title'] if len(r['title']) <= 30 else r['title'][:29] + '…'
+ s.append('%s%s' % (left - 10, y + h / 2 + 4, e(title), e(r['title'])))
+ x = left
+ segs = [k for k in STATUSES if r[k]]
+ for j, k in enumerate(segs):
+ last = j == len(segs) - 1
+ w = sx(r[k]) - (0 if last else 2) # a 2px gap between the segments
+ tip = '%s\n%d %s of %d tests' % (r['title'], r[k], k, r['n'])
+ s.append(hbar(x, y, w, h, 4, COLOR[k], tip) if last else
+ '%s' % (x, y, w, h, COLOR[k], e(tip)))
+ x += sx(r[k])
+ s.append('%d/%d' % (x + 8, y + h / 2 + 4, r['PASS'], r['n']))
+ return (''
+ % (width, len(rows) * rh + 28, ''.join(s)))
+
+
+def trend_chart(runs):
+ width, left, base, top = 900, 34, 190, 14
+ n = len(runs)
+ bw = min(30, (width - left - 30) / n * 0.6)
+ gap = min((width - left - 20 - n * bw) / max(n - 1, 1), 30)
+ big = max(len(r['results']) for r in runs) or 1
+ step = 5 if big <= 25 else 10 if big <= 60 else 50
+ ymax = -(-big // step) * step
+ sy = lambda v: v / ymax * (base - top)
+ s = []
+ for v in range(0, ymax + 1, step):
+ s.append('%d'
+ % (left, width - 10, base - sy(v), base - sy(v), left - 6, base - sy(v) + 4, v))
+ for i, run in enumerate(runs):
+ x, cur = left + 6 + i * (bw + gap), i == n - 1
+ c = {k: count(run['results'], k) for k in STATUSES}
+ tip = 'Run %s · %s%s\n%d pass · %d fail · %d skip' % (run['started'][:16].replace('T', ' '), run['sha'][:8],
+ ' (aborted)' if run.get('aborted') else '', c['PASS'], c['FAIL'], c['SKIP'])
+ y0 = base
+ segs = [k for k in STATUSES if c[k]]
+ for j, k in enumerate(segs):
+ last = j == len(segs) - 1
+ s.append(vbar(x, y0, bw, sy(c[k]) - (0 if last else 2), 4, COLOR[k], tip, last))
+ y0 -= sy(c[k])
+ if c['FAIL']:
+ s.append('%d' % (x + bw / 2, y0 - 6, c['FAIL']))
+ s.append('%s' % ('b' if cur else 'm', x + bw / 2, base + 16, short_date(run['started'])))
+ s.append('%s' % (x + bw / 2, base + 30, e(run['sha'][:6])))
+ if cur:
+ s.append('this run' % (x + bw / 2, base + 46))
+ return (''
+ % (width, base + 52, ''.join(s)))
+
+
+def describe_history(h, runs):
+ """What a row of the status history says: flaky, failing since, fixed since..."""
+ seen = [x for x in h if x]
+ last = h[-1]
+ changes = sum(1 for a, b in zip(seen, seen[1:]) if a != b)
+ since = len(h) - 1
+ while since > 0 and h[since - 1] == last:
+ since -= 1
+ when = '%s %s (%s)' % (short_date(runs[since]['started']), runs[since]['started'][11:16], runs[since]['sha'][:6])
+ if all(x == 'SKIP' for x in seen):
+ return 'never ran'
+ if changes >= 4:
+ return 'flaky: %d changes' % changes
+ if last == 'FAIL':
+ return 'new failure' if since == len(h) - 1 else 'failing since ' + when
+ if last == 'SKIP':
+ return 'skipped since ' + short_date(runs[since]['started'])
+ return 'fixed since ' + when if last == 'PASS' else ''
+
+
+def history_sections(run, history):
+ runs = history[-(HISTORY_RUNS - 1):] + [run]
+ status = [{r['name']: r['status'] for r in x['results']} for x in runs]
+ prev = status[-2]
+ cur = run['results']
+ new = [r for r in cur if r['status'] == 'FAIL' and prev.get(r['name']) != 'FAIL']
+ fixed = [r for r in cur if r['status'] == 'PASS' and prev.get(r['name']) == 'FAIL']
+ still = [r for r in cur if r['status'] == 'FAIL' and prev.get(r['name']) == 'FAIL']
+ line = lambda r, tag: '
%s%s%s
' % (tag, e(r['name']), e(r['file']))
+ changes = ''.join([line(r, 'NEW ' + chip('FAIL')) for r in new] +
+ [line(r, chip('PASS')) for r in fixed] +
+ [line(r, '✗ STILL FAILING') for r in still]) or \
+ '
'
+ % (e(p['started'][:16].replace('T', ' ')), e(p['sha'][:10]), changes)]
+ rows = [r for r in cur if any(s.get(r['name'], 'PASS') != 'PASS' for s in status)]
+ if rows:
+ head = '
%s
' % ''.join(
+ '
%s
' % (' cur' if i == len(runs) - 1 else '', short_date(x['started'])) for i, x in enumerate(runs))
+ body = []
+ for r in rows:
+ h = [s.get(r['name']) for s in status]
+ cells = ''.join('
%s
' % (
+ x or '', ' cur' if i == len(runs) - 1 else '',
+ e('%s · %s %s: %s' % (r['name'], short_date(runs[i]['started']), runs[i]['sha'][:8], x or 'not in this run')), ICON[x])
+ for i, x in enumerate(h))
+ body.append('
The tests that did not pass in every run. '
+ 'The framed column is this run.
%s%s
'
+ % (head, ''.join(body)))
+ return out, len(new) - len(fixed), len(new), len(fixed)
+
+
+def film(r, outdir):
+ """The screenshots after the last steps before the failure, and the final screenshot."""
+ shots = [(s['file'], 'Step %d' % s['step'], s['label'], False) for s in r.get('shots', [])][-3:]
+ if os.path.exists(os.path.join(outdir, r['slug'] + '.png')):
+ at = r.get('failed_step', 0)
+ shots.append((r['slug'] + '.png', 'Step %d · failed here' % at if at else 'At the end', '', True))
+ return '
' % (
+ ' bad' if bad else '', e(f), e(f), e(title), e(title), e(label)) for f, title, label, bad in shots) if shots else ''
+
+
+def problem_cards(run, outdir):
+ rs = [r for r in run['results'] if r['status'] != 'PASS']
+ rs.sort(key=lambda r: r['status'] != 'FAIL')
+ return ''.join('
%s%s
%s
%s%s
'
+ '%s
%s
%s
' % (
+ r['status'], chip(r['status']), crash_chip(r), e(r['name']), e(r['file']), links(r, outdir),
+ strip(r) if r['status'] == 'FAIL' else '', e(r['reason']), film(r, outdir) if r['status'] == 'FAIL' else '')
+ for r in rs)
+
+
+def all_tests(run, outdir):
+ rows = []
+ for f in run['files']:
+ rs = [r for r in run['results'] if r['file'] == f['file']]
+ if not rs:
+ continue
+ rows.append('
%s · %s · %d of %d passed
' % (
+ e(f['title'] or f['file']), e(f['file']), count(rs, 'PASS'), len(rs)))
+ for r in rs:
+ shots = r.get('shots', [])
+ more = ('screenshots after the steps (%d)
' % ''.join(tiles)]
+ if len([f for f in run['files'] if any(r['file'] == f['file'] for r in rs)]) > 1:
+ out.append('
Results per test file
Which part of the GUI has problems.
%s%s'
+ % (legend(), per_file_chart(run)))
+ out += hist
+ if c['FAIL'] or c['SKIP']:
+ out.append('
Failed and skipped tests
The reason, how far the test got, and the screenshots '
+ 'after the last steps. The red frame is the screen when the test failed.
%s' % problem_cards(run, outdir))
+ if rs:
+ out.append('
All tests
Grouped by test file. The strip shows the steps: green passed, '
+ 'red failed, grey not run.
%s' % all_tests(run, outdir))
+ if not history:
+ out.append('
No earlier runs of this edition with results.json were found in %s, '
+ 'the history is shown from the next run.
' % e(os.path.dirname(outdir)))
+ out.append('')
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write('\n'.join(out) + '\n')
diff --git a/gui/manualtest/run.py b/gui/manualtest/run.py
new file mode 100755
index 00000000000..657cd6ce0a2
--- /dev/null
+++ b/gui/manualtest/run.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+"""Run the manual tests in a test file by letting Claude drive the GUI.
+
+ run.py APP FILE.feature... [--edition opensource|premium] [--test SUBSTRING] [--id ID] [--src DIR] [--out DIR]
+
+FILE.feature is a Gherkin test file, edited with web.py or by hand, see testfile.py. The scripts
+are in FILE.scripts.json. For each scenario (and each row of its Examples) the application is started
+fresh (empty HOME) and the script of the test is run with guitest.py commands, see runner.py.
+No AI is involved. With --edition APP is that edition and only the tests for it are run.
+Tests without a script are SKIP. A step that fails gives PASS -> FAIL with the
+step, the reason and the elements that are on the screen.
+"""
+
+import argparse
+import os
+import re
+import datetime
+import json
+import platform
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+
+import report
+import runner
+import testfile
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+GUITEST = os.path.join(HERE, 'guitest.py')
+
+def parse(path, edition=None):
+ return [dict(t, file=os.path.basename(path)) for t in testfile.expand(path, edition)]
+
+
+CRASH_RE = re.compile(r'exited with status (?!0\b|143\b)\d+|Segmentation fault|SIGSEGV|SIGABRT|Aborted|core dumped|terminate called|'
+ r'QFATAL|Fatal error|AddressSanitizer|runtime error:|Assertion .* failed', re.I)
+
+
+def screenshot(sub, path):
+ """Save a screenshot without the empty (black) part of the screen right of and below the windows.
+ The top left corner stays, so positions in the image are screen positions."""
+ if sub('screenshot', path, capture_output=True).returncode:
+ return False
+ try:
+ from PIL import Image
+ img = Image.open(path)
+ box = img.convert('RGB').getbbox()
+ if box and (box[2] < img.width or box[3] < img.height):
+ img.crop((0, 0, box[2], box[3])).save(path)
+ except (ImportError, OSError):
+ pass
+ return True
+
+
+def detect_crash(sub, outdir, slug):
+ """Return a description when the application died or logged a fatal error, else ''."""
+ log = sub('log', capture_output=True).stdout
+ with open(os.path.join(outdir, slug + '.app.log'), 'w') as f:
+ f.write(log)
+ hits = [l.strip() for l in log.splitlines() if CRASH_RE.search(l)]
+ dead = sub('alive', capture_output=True).returncode != 0
+ if dead and re.search(r'application exited with status 0\b', log):
+ dead = False # the test closed the application (Quit)
+ if not dead and not hits:
+ return ''
+ why = 'application is not running at the end of the test' if dead else 'fatal message in application log'
+ return why + (': ' + hits[0] if hits else '')
+
+
+def emit(kind, **data):
+ """One line of progress for the web interface, with --events."""
+ if EVENTS:
+ print('@' + json.dumps(dict(data, kind=kind)), flush=True)
+
+
+EVENTS = False
+
+
+def run_test(app, test, outdir, on_step=None):
+ """Returns {status, reason, crash, steps, failed_step, shots}. failed_step is 0 when no step failed,
+ shots are the screenshots after the steps: [{step, label, file}]."""
+ slug = re.sub(r'\W+', '_', test['name'])
+ shots, reached = [], [0]
+ result = lambda status, reason, crash='', failed=0: {'status': status, 'reason': reason, 'crash': crash,
+ 'steps': len(test['script']), 'failed_step': failed, 'shots': shots}
+ if test.get('stale'):
+ return result('SKIP', 'OUTDATED SCRIPT: the test was changed after the script was made. '
+ 'Generate the script again (gen.py or the web interface) or approve it there.')
+ if not test['script']:
+ return result('SKIP', 'no script, make one with gen.py or in the web interface')
+ work = tempfile.mkdtemp(prefix='guitest-work-')
+ state = os.path.join(work, 'state.json')
+ env = dict(os.environ, GUITEST_STATE=state)
+ sub = lambda *a, **kw: subprocess.run([sys.executable, GUITEST, *a], env=env, text=True, **kw)
+ try:
+ variables = testfile.materialize(test['root'], test['projects'], work) # same files every run
+ except FileNotFoundError as e:
+ return result('FAIL', str(e))
+ r = sub('start', app, capture_output=True)
+ if r.returncode:
+ return result('FAIL', 'could not start: ' + (r.stdout + r.stderr).strip())
+ try:
+ with open(state) as f:
+ home = json.load(f)['env']['HOME']
+ def shot(i, label):
+ name = '%s.step%02d.png' % (slug, i)
+ if not screenshot(sub, os.path.join(outdir, name)):
+ return None
+ shots.append({'step': i, 'label': label, 'file': name})
+ return name
+ def step(i, n, label):
+ reached[0] = i
+ if on_step:
+ on_step(i, n, label)
+ ok, reason, log = runner.run_script(sub, test['script'], work, home, variables, step, shot)
+ with open(os.path.join(outdir, slug + '.txt'), 'w') as f:
+ f.write(log)
+ screenshot(sub, os.path.join(outdir, slug + '.png'))
+ crash = detect_crash(sub, outdir, slug)
+ status = 'PASS' if ok else 'FAIL'
+ if crash:
+ # a crash is a failure even if all the steps passed
+ status, reason = 'FAIL', 'APPLICATION CRASHED: %s\n%s' % (crash, reason)
+ return result(status, reason, crash, 0 if ok else reached[0])
+ finally:
+ sub('stop', capture_output=True)
+
+
+def git(directory, *a):
+ r = subprocess.run(['git', '-C', directory, *a], text=True, capture_output=True)
+ return r.stdout.strip() if r.returncode == 0 else ''
+
+
+def app_info(app, src):
+ """git info for the tested application, taken from SRC (default: the directory of the binary)."""
+ d = os.path.abspath(src) if src else os.path.dirname(app)
+ sha = git(d, 'rev-parse', 'HEAD')
+ return {
+ 'path': app,
+ 'sha': sha or 'unknown',
+ 'branch': git(d, 'rev-parse', '--abbrev-ref', 'HEAD') or 'unknown',
+ 'subject': git(d, 'log', '-1', '--format=%s') if sha else '',
+ 'dirty': bool(git(d, 'status', '--porcelain', '--untracked-files=no')) if sha else False,
+ }
+
+
+def load_history(out, edition, current):
+ """results.json of the earlier runs of EDITION in OUT, oldest first."""
+ runs = []
+ for d in sorted(os.listdir(out)):
+ path = os.path.join(out, d, 'results.json')
+ if d == current or not os.path.isfile(path):
+ continue
+ try:
+ with open(path, encoding='utf-8') as f:
+ run = json.load(f)
+ except (OSError, ValueError):
+ continue
+ if run.get('edition') == edition:
+ runs.append(run)
+ return runs[-(report.HISTORY_RUNS - 1):]
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('app')
+ ap.add_argument('file', nargs='+', help='one or more .feature test files')
+ ap.add_argument('--edition', choices=testfile.EDITIONS, help='the edition that APP is, only the tests for it')
+ ap.add_argument('--test', help='only tests whose name contains this')
+ ap.add_argument('--id', help='only the test with this id (all the rows of its Examples)')
+ ap.add_argument('--src', help='git checkout of the application (default: the directory of APP)')
+ ap.add_argument('--out', default=os.path.join(HERE, 'results'))
+ ap.add_argument('--events', action='store_true', help='progress lines for the web interface')
+ args = ap.parse_args()
+ global EVENTS
+ EVENTS = args.events
+ signal.signal(signal.SIGINT, signal.default_int_handler) # also when started in the background
+
+ started = datetime.datetime.now()
+ info = app_info(os.path.abspath(args.app), args.src)
+ # each run gets its own directory: results/[-]-/
+ parent = args.out
+ args.out = os.path.join(args.out, '%s%s-%s' % (started.strftime('%Y%m%d-%H%M%S'),
+ '-' + args.edition if args.edition else '', info['sha'][:8]))
+ os.makedirs(args.out, exist_ok=True)
+ failed = 0
+ results = []
+ aborted = False
+ try:
+ selected = [t for f in args.file for t in parse(f, args.edition)
+ if not (args.test and args.test.lower() not in t['name'].lower() or args.id and t['id'] != args.id)]
+ emit('start', total=len(selected), out=args.out, edition=args.edition)
+ for index, test in enumerate(selected, 1):
+ name = test['name']
+ emit('test', index=index, name=name, steps=len(test['script']))
+ t0 = time.time()
+ try:
+ res = run_test(os.path.abspath(args.app), test, args.out,
+ lambda k, n, label: emit('step', step=k, steps=n, label=label))
+ except KeyboardInterrupt:
+ results.append({'name': name, 'file': test['file'], 'status': 'SKIP', 'reason': 'aborted by user', 'crash': '',
+ 'seconds': time.time() - t0, 'slug': re.sub(r'\W+', '_', name), 'steps': 0, 'failed_step': 0, 'shots': []})
+ emit('result', **results[-1])
+ raise
+ results.append(dict(res, name=name, file=test['file'], seconds=time.time() - t0, slug=re.sub(r'\W+', '_', name)))
+ emit('result', **results[-1])
+ print('%-4s %s: %s' % (res['status'], name, res['reason']), flush=True)
+ failed += res['status'] == 'FAIL'
+ except KeyboardInterrupt:
+ # the report is written with what is done, tests not started are left out
+ aborted = True
+ print('aborted')
+ run = {'started': started.isoformat(timespec='seconds'), 'edition': args.edition, 'app': info['path'],
+ 'sha': info['sha'], 'dirty': info['dirty'], 'branch': info['branch'], 'subject': info['subject'],
+ 'host': '%s, %s' % (platform.node(), platform.platform()), 'aborted': aborted,
+ 'files': [{'file': os.path.basename(f), 'title': testfile.load(f).get('title', '')} for f in args.file],
+ 'results': results}
+ with open(os.path.join(args.out, 'results.json'), 'w', encoding='utf-8') as f:
+ json.dump(run, f, indent=1)
+ path = os.path.join(args.out, 'report.html')
+ report.write(path, run, load_history(parent, args.edition, os.path.basename(args.out)))
+ print('report: ' + path)
+ emit('done', report=path)
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/gui/manualtest/runner.py b/gui/manualtest/runner.py
new file mode 100644
index 00000000000..11634cdb1e4
--- /dev/null
+++ b/gui/manualtest/runner.py
@@ -0,0 +1,232 @@
+"""Executes a test script (see testfile.SCRIPT_DOC) with guitest.py commands, no AI involved.
+
+run_script() gets `sub`, a function that runs one guitest.py command in the session of the test:
+sub(cmd, *args, capture_output=True) -> CompletedProcess with returncode, stdout, stderr.
+"""
+
+import os
+import re
+import shutil
+import time
+
+import testfile
+
+# actions that change the GUI, a screenshot is taken after each of them
+GUI_ACTIONS = ('click', 'rightclick', 'doubleclick', 'focus', 'hover', 'settext', 'type', 'key', 'restart')
+NO_ELEMENT = ('no matching visible element', 'does not answer on the accessibility bus')
+DEFAULT_TIMEOUT = 5
+
+
+def selector_args(step):
+ a = []
+ for k in ('name', 'role', 'path'):
+ if k in step:
+ a += ['--' + k, str(step[k])]
+ if 'nth' in step:
+ a += ['--nth', str(step['nth'])]
+ if step.get('exact'):
+ a.append('--exact')
+ return a
+
+
+def describe(step):
+ return ' '.join('%s=%r' % (k, v) for k, v in step.items() if k != 'label')
+
+
+def message(p):
+ text = (p.stderr.strip() or p.stdout.strip() or 'exit code %d' % p.returncode).removeprefix('error: ')
+ return text.replace('; try "tree --showing"', '')
+
+
+class StepFailed(Exception):
+ pass
+
+
+def check_path(path, allowed):
+ path = os.path.realpath(path)
+ if not any(path == d or path.startswith(d + os.sep) for d in (os.path.realpath(x) for x in allowed)):
+ raise StepFailed('path %s is outside the test directories' % path)
+ return path
+
+
+def gui(sub, cmd, step, extra=()):
+ """Run a command that needs an element. The element may not have appeared yet, so retry."""
+ deadline = time.time() + step.get('timeout', DEFAULT_TIMEOUT)
+ while True:
+ p = sub(cmd, *selector_args(step), *extra, capture_output=True)
+ if p.returncode == 0:
+ return p
+ if not any(m in message(p) for m in NO_ELEMENT) or time.time() > deadline:
+ raise StepFailed(message(p))
+ time.sleep(0.4)
+
+
+def run_step(sub, step, dirs, captured):
+ action = step.get('do') or step['expect']
+ if 'do' in step:
+ if action in ('click', 'rightclick', 'doubleclick', 'focus', 'hover'):
+ gui(sub, action, step, ['--mouse'] if step.get('mouse') else [])
+ elif action == 'settext':
+ gui(sub, 'settext', step, [step['value']])
+ elif action == 'type':
+ ok(sub('type', step['value'], capture_output=True))
+ elif action == 'key':
+ keys = step['keys'].split() if isinstance(step['keys'], str) else step['keys']
+ ok(sub('key', *keys, capture_output=True))
+ elif action == 'wait':
+ ok(sub('wait', *selector_args(step), '--timeout', str(step.get('timeout', 10)),
+ *(['--gone'] if step.get('gone') else []), capture_output=True))
+ elif action == 'restart':
+ ok(sub('restart', capture_output=True))
+ elif action == 'sleep':
+ time.sleep(float(step['seconds']))
+ elif action == 'copy':
+ src, dst = check_path(step['from'], dirs), check_path(step['to'], dirs)
+ if not os.path.isfile(src):
+ raise StepFailed('file %s does not exist' % src)
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
+ shutil.copyfile(src, dst)
+ elif action == 'write':
+ path = check_path(step['path'], dirs)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(step['content'])
+ return
+ if action == 'visible':
+ ok(sub('wait', *selector_args(step), '--timeout', str(step.get('timeout', DEFAULT_TIMEOUT)),
+ capture_output=True))
+ elif action == 'gone':
+ ok(sub('wait', *selector_args(step), '--gone', '--timeout', str(step.get('timeout', DEFAULT_TIMEOUT)),
+ capture_output=True))
+ elif action == 'state':
+ info = gui(sub, 'info', step).stdout
+ have = set((re.search(r'\[([\w,]*)\]\s*$', info.strip()) or [None, ''])[1].split(','))
+ if 'state' in step and step['state'] not in have:
+ raise StepFailed('element is not %s, its states are [%s]: %s' % (step['state'], ','.join(sorted(have)), info.strip()))
+ if 'not_state' in step and step['not_state'] in have:
+ raise StepFailed('element is %s but should not be: %s' % (step['not_state'], info.strip()))
+ elif action == 'value':
+ info = gui(sub, 'info', step).stdout.strip()
+ found = re.findall(r" value=(\S+)", info) + re.findall(r" text='(.*?)'(?= value=| \[|$)", info)
+ if not any(same(v, step['equals']) for v in found):
+ raise StepFailed('the value is %s, not %r: %s' % (found or 'missing', step['equals'], info))
+ elif action == 'exited':
+ deadline = time.time() + step.get('timeout', DEFAULT_TIMEOUT)
+ while sub('alive', capture_output=True).returncode == 0:
+ if time.time() > deadline:
+ raise StepFailed('the application is still running')
+ time.sleep(0.3)
+ status = re.findall(r'application exited with status (\d+)', sub('log', capture_output=True).stdout)
+ if status and status[-1] != '0':
+ raise StepFailed('the application ended with status %s, not 0' % status[-1])
+ elif action == 'text':
+ deadline = time.time() + step.get('timeout', DEFAULT_TIMEOUT) # the screen may take a moment to change
+ while True:
+ texts = sub('texts', capture_output=True).stdout
+ problem = None
+ if 'contains' in step and step['contains'] not in texts:
+ problem = 'the text %r is not on the screen' % step['contains']
+ elif 'not_contains' in step and step['not_contains'] in texts:
+ problem = 'the text %r is on the screen but should not be' % step['not_contains']
+ if not problem:
+ break
+ if time.time() > deadline:
+ raise StepFailed(problem)
+ time.sleep(0.4)
+ elif action == 'ocr':
+ deadline = time.time() + step.get('timeout', DEFAULT_TIMEOUT)
+ norm = lambda x: re.sub(r'\s+', ' ', x).lower()
+ while True:
+ p = sub('ocr', capture_output=True)
+ if p.returncode:
+ raise StepFailed(message(p))
+ seen = norm(p.stdout)
+ problem = None
+ if 'contains' in step and norm(step['contains']) not in seen:
+ problem = 'the text %r is not on the screen (OCR)' % step['contains']
+ elif 'not_contains' in step and norm(step['not_contains']) in seen:
+ problem = 'the text %r is on the screen (OCR) but should not be' % step['not_contains']
+ if not problem:
+ break
+ if time.time() > deadline:
+ raise StepFailed(problem + '. OCR read: ' + seen[:300])
+ time.sleep(0.4)
+ elif action == 'file':
+ path = check_path(step['path'], dirs)
+ try:
+ with open(path, encoding='utf-8', errors='replace') as f:
+ text = f.read()
+ except FileNotFoundError:
+ raise StepFailed('file %s does not exist' % path)
+ if 'contains' in step and step['contains'] not in text:
+ raise StepFailed('%s does not contain %r' % (path, step['contains']))
+ if 'not_contains' in step and step['not_contains'] in text:
+ raise StepFailed('%s contains %r but should not' % (path, step['not_contains']))
+ if 'regex' in step and not re.search(step['regex'], text, re.M):
+ raise StepFailed('%s does not match /%s/' % (path, step['regex']))
+ elif action == 'alive':
+ ok(sub('alive', capture_output=True))
+ elif action == 'capture':
+ info = gui(sub, 'info', step).stdout.strip()
+ m = re.search(step['regex'], info)
+ if not m:
+ raise StepFailed('/%s/ does not match: %s' % (step['regex'], info))
+ captured[step['as']] = m.group(1) if m.groups() else m.group(0)
+
+
+def same(a, b):
+ try:
+ return float(a) == float(b) # a spin box says 1.0
+ except ValueError:
+ return a == b
+
+
+def ok(p):
+ if p.returncode:
+ raise StepFailed(message(p))
+
+
+def context(sub, step):
+ """What the screen looks like when a step failed, to make a renamed button easy to find."""
+ out = []
+ if {'name', 'role', 'path'} & set(step):
+ cmd = ['suggest'] + ['--%s=%s' % (k, step[k]) for k in ('name', 'role') if k in step]
+ s = sub(*cmd, capture_output=True).stdout.strip()
+ if s:
+ out.append('most similar visible elements:\n' + '\n'.join(' ' + l for l in s.splitlines()))
+ w = sub('windows', capture_output=True).stdout.strip()
+ if w:
+ out.append('windows:\n' + '\n'.join(' ' + l.split(' @')[0] for l in w.splitlines()[:6]))
+ return '\n '.join(out)
+
+
+
+def run_script(sub, script, work, home, variables=None, progress=None, shot=None):
+ """Returns (ok, message, log). On failure the message names the step and what was on screen.
+ shot(i, label) is called after each step i that is one of GUI_ACTIONS and returns the name of the
+ screenshot it saved, or None."""
+ dirs = [work, home]
+ variables = dict(variables or {}, work=work, home=home)
+ captured = {} # {name} values of the capture steps so far
+ log = []
+ for i, step in enumerate(script, 1):
+ step = testfile.subst_deep(step, dict(variables, **captured))
+ t0 = time.time()
+ label = step.get('label')
+ if progress:
+ progress(i, len(script), label or describe(step))
+ head = 'step %d/%d%s: %s' % (i, len(script), ' "%s"' % label if label else '', describe(step))
+ try:
+ run_step(sub, step, dirs, captured)
+ except StepFailed as e:
+ log.append('FAIL %s (%.1f s)' % (head, time.time() - t0))
+ msg = '%s\n %s' % (head, e)
+ ctx = context(sub, step)
+ return False, msg + ('\n ' + ctx if ctx else ''), '\n'.join(log) + '\n' + msg + '\n'
+ log.append('ok %s (%.1f s)' % (head, time.time() - t0))
+ if shot and step.get('do') in GUI_ACTIONS:
+ time.sleep(0.2) # let the application paint
+ name = shot(i, label or describe(step))
+ if name:
+ log[-1] += ' screenshot ' + name
+ return True, 'all %d steps passed' % len(script), '\n'.join(log) + '\n'
diff --git a/gui/manualtest/testfile.py b/gui/manualtest/testfile.py
new file mode 100644
index 00000000000..a915e7960fa
--- /dev/null
+++ b/gui/manualtest/testfile.py
@@ -0,0 +1,743 @@
+#!/usr/bin/env python3
+"""Test files for run.py, written in Gherkin (FILE.feature), with the scripts in FILE.scripts.json.
+
+ Feature: Project file dialog
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t1a2b3c4d @ticket:14983 @project:relative-paths
+ Scenario: Relative paths
+ TODO: cannot be automated optional, a description line that starts with TODO:
+ When I open the project dialog
+ And I select the platform
+ Then relative paths are used in the XML
+
+ Examples:
+ | platform | lang |
+ | native | C |
+ | unix32 | C++ |
+
+Feature: is the title of the file. Background: steps are done before every scenario of the file.
+A scenario has steps that start with Given, When, Then, And, But or *, at least one Then (the
+expected result). A step can be followed by a doc string (lines between \"\"\") or a table (| a | b |).
+Lines that start with # are comments, they are not kept when the file is saved by web.py.
+
+Tags of a scenario (all optional):
+ @id:ID identifies a test when it is renamed, added when it is missing
+ @ticket:NUMBER the ticket of the test (#NUMBER)
+ @project:NAME a test project, see below; one tag per project
+ @edition:NAME an edition that the test is for, one of EDITIONS; one tag per edition, none: all
+ @disabled the test is not run
+Other tags are kept but not used.
+
+A run is for one edition (run.py --edition, with the binary of that edition), the tests that are
+not for it are left out. The script is the same for all editions, {edition} in the steps and the
+script is the name of the edition of the run.
+
+Examples (Scenario Outline) are the test matrix: the test is run once for every row of the table.
+ in the steps and {column} in the script are replaced by the value of the row.
+
+"script" is what run.py executes, see SCRIPT_DOC below. It is written by gen.py (an AI explores
+the GUI once) or by hand, and is then replayed without any AI. The scripts are kept in
+FILE.scripts.json, {"ID": {"script": [...], "script_for": "..."}}, next to the feature file.
+
+@project:NAME are test projects, directories with files and code in DIR/projects/NAME,
+shared by all tests next to them. Before every run they are copied to an empty temporary directory,
+so a test always starts with the same files: {project:NAME} in steps and scripts is the copy of NAME,
+and {project} when the test has exactly one project.
+
+A script is made for the steps (with the Background), todo, examples and test projects (the files in
+them) of a test. "script_for" is a hash of those at the time, when they are changed afterwards the
+script is OUTDATED: it is kept but not run until it is generated again, edited or approved.
+
+In Python a test file is a dict {"title", "background": [lines], "tests": [test]}, a test is
+{"id", "name", "steps": [lines], "todo", "ticket", "enabled", "projects", "editions", "examples":
+[{column: value}], "script", "script_for"}. The lines of the steps are Gherkin without indentation.
+
+ testfile.py convert IN.md|IN.json OUT.feature import a markdown or an older JSON test file
+"""
+
+import hashlib
+import itertools
+import json
+import os
+import re
+import shutil
+import sys
+
+AXIS_RE = re.compile(r'^\w+$')
+PROJECT_RE = re.compile(r'^[\w-]+$')
+PROJECTS = 'projects' # test projects: one directory per project, next to the test files
+EDITIONS = ('opensource', 'premium')
+EXT = '.feature'
+SCRIPTS_EXT = '.scripts.json'
+
+SCRIPT_DOC = """A script is a list of steps. Every step is an object with "do" (an action) or "expect" (a check),
+and an optional "label" that says what the step is for (shown when it fails). Elements are found
+with name (substring of the accessible name/text, case-insensitive), role, path, nth, exact, like
+the guitest.py commands. {work} is a temporary directory for files, {home} the HOME of the
+application, {project:NAME} the copy of the test project NAME ({project} when the test has just one),
+use those for the files of a test instead of write steps. {column} values of the Examples row and {edition}
+(opensource or premium, the same script is used for both) are replaced before the run.
+
+actions
+ {"do": "click|rightclick|doubleclick|focus|hover", "name": "...", "role": "...", "mouse": true}
+ {"do": "settext", "name": "...", "value": "..."} set the text of an edit field
+ {"do": "type", "value": "..."} keyboard input to the focused element
+ {"do": "key", "keys": ["ctrl+o", "Return"]} xdotool key names
+ {"do": "wait", "name": "...", "gone": true, "timeout": 10}
+ {"do": "restart"} quit and start the application again
+ {"do": "sleep", "seconds": 1}
+ {"do": "write", "path": "{work}/a.c", "content": "..."}
+ {"do": "copy", "from": "{project:p}/a_v2.c", "to": "{project:p}/a.c"} change a file of the project
+checks, the test fails when one is wrong
+ {"expect": "visible", "name": "...", "role": "...", "timeout": 5}
+ {"expect": "gone", "name": "..."}
+ {"expect": "state", "name": "...", "state": "checked"} or "not_state": "checked"; also
+ showing enabled focused selected expanded
+ {"expect": "value", "name": "...", "equals": "1"} value or text of a field, a spin box...
+ {"expect": "exited"} the application has ended by itself, status 0 (Quit)
+ {"expect": "text", "contains": "..."} or "not_contains": anywhere on screen
+ {"expect": "ocr", "contains": "Recheck"} text on the screen read by OCR (tesseract), or "not_contains";
+ only for what "text" cannot see, e.g. a context menu that
+ is not in the accessibility tree. Case is ignored.
+ {"expect": "file", "path": "{home}/x.xml", "contains": "...", "not_contains": "...", "regex": "..."}
+ {"expect": "alive"}
+ {"expect": "capture", "name": "...", "role": "frame", "regex": "'Cppcheck (\\d[^']*)'", "as": "version"}
+ the regex is searched in the element's info line
+ (role 'name' text='...' value=...), group 1 (or the
+ whole match) is stored and {version} in the later
+ steps is replaced by it; use it instead of hard coding
+ text that changes, e.g. the version
+A check can have "observed_fail": true when the application was seen not to do what the check says
+(a known failure, only for the check that failed). Text checks wait up to "timeout" seconds (default 5).
+Actions that need an element wait up to "timeout" seconds (default 5) for it to appear."""
+
+SELECTOR = {'name', 'role', 'path', 'nth', 'exact'}
+# step name -> (required keys, allowed optional keys)
+STEPS = {
+ 'do': {
+ 'click': ((), SELECTOR | {'mouse'}), 'rightclick': ((), SELECTOR), 'doubleclick': ((), SELECTOR),
+ 'focus': ((), SELECTOR), 'hover': ((), SELECTOR),
+ 'settext': (('value',), SELECTOR), 'type': (('value',), set()), 'key': (('keys',), set()),
+ 'wait': ((), SELECTOR | {'gone'}), 'restart': ((), set()),
+ 'sleep': (('seconds',), set()), 'write': (('path', 'content'), set()), 'copy': (('from', 'to'), set()),
+ },
+ 'expect': {
+ 'visible': ((), SELECTOR), 'gone': ((), SELECTOR), 'value': (('equals',), SELECTOR), 'exited': ((), set()),
+ 'state': ((), SELECTOR | {'state', 'not_state'}),
+ 'text': ((), {'contains', 'not_contains'}), 'file': (('path',), {'contains', 'not_contains', 'regex'}),
+ 'alive': ((), set()), 'ocr': ((), {'contains', 'not_contains'}),
+ 'capture': (('regex', 'as'), SELECTOR),
+ },
+}
+NEEDS_SELECTOR = {'click', 'rightclick', 'doubleclick', 'focus', 'hover', 'settext', 'wait', 'visible', 'gone', 'state', 'value',
+ 'capture'}
+STATES = {'showing', 'enabled', 'focused', 'selected', 'expanded', 'checked', 'editable', 'modal'}
+
+
+def validate_script(script, where='script'):
+ if not isinstance(script, list):
+ return [where + ': must be a list of steps']
+ errors = []
+ for i, step in enumerate(script, 1):
+ w = '%s step %d' % (where, i)
+ if not isinstance(step, dict):
+ errors.append(w + ': not an object')
+ continue
+ kinds = [k for k in ('do', 'expect') if k in step]
+ if len(kinds) != 1:
+ errors.append(w + ': needs exactly one of "do" or "expect"')
+ continue
+ kind, action = kinds[0], step[kinds[0]]
+ if action not in STEPS[kind]:
+ errors.append('%s: unknown %s "%s", use one of %s' % (w, kind, action, ', '.join(STEPS[kind])))
+ continue
+ required, optional = STEPS[kind][action]
+ allowed = set(required) | optional | {kind, 'label', 'timeout'} | ({'observed_fail'} if kind == 'expect' else set())
+ for k in step:
+ if k not in allowed:
+ errors.append('%s: "%s" is not used by %s %s' % (w, k, kind, action))
+ for k in required:
+ if k not in step:
+ errors.append('%s: %s %s needs "%s"' % (w, kind, action, k))
+ if action in NEEDS_SELECTOR and not (SELECTOR - {'nth', 'exact'}) & set(step):
+ errors.append('%s: %s %s needs name, role or path' % (w, kind, action))
+ if action in ('text', 'ocr') and not {'contains', 'not_contains'} & set(step):
+ errors.append(w + ': expect %s needs contains or not_contains' % action)
+ if action == 'state' and not {'state', 'not_state'} & set(step):
+ errors.append(w + ': expect state needs state or not_state')
+ for k in ('state', 'not_state'):
+ if k in step and step[k] not in STATES:
+ errors.append('%s: unknown state "%s", use one of %s' % (w, step[k], ', '.join(sorted(STATES))))
+ if action == 'capture':
+ try:
+ re.compile(step['regex'])
+ except (re.error, TypeError) as e:
+ errors.append('%s: bad regex: %s' % (w, e))
+ if not re.fullmatch(r'[\w-]+', str(step['as'])):
+ errors.append(w + ': "as" must be a name of letters, digits, _ and -')
+ if script and not any('expect' in s for s in script if isinstance(s, dict)):
+ errors.append(where + ': needs at least one "expect" step, otherwise nothing is tested')
+ return errors
+
+
+STEP_RE = re.compile(r'^(Given|When|Then|And|But|\*)\s+\S')
+SCENARIO_RE = re.compile(r'^(Scenario Outline|Scenario Template|Scenario|Example):\s*(.*)$')
+TAG_RE = re.compile(r'^@[^\s@]+$')
+DOCSTRING = ('"""', '```')
+
+
+class ParseError(ValueError):
+ pass
+
+
+def validate_steps(lines, where, need_then=True):
+ """Errors in the Gherkin lines of a scenario or background."""
+ errors, fence, keyword = [], None, None
+ for n, line in enumerate(lines, 1):
+ s = line.strip()
+ if fence:
+ if s == fence:
+ fence = None
+ continue
+ if s.startswith(DOCSTRING) or s.startswith('|'):
+ if keyword is None:
+ errors.append('%s line %d: a doc string or table must come after a step' % (where, n))
+ if s.startswith(DOCSTRING):
+ fence = s[:3]
+ continue
+ m = STEP_RE.match(s)
+ if not m:
+ errors.append('%s line %d: a step must start with Given, When, Then, And, But or *: %s' % (where, n, s))
+ continue
+ if keyword is None and m.group(1) in ('And', 'But'):
+ errors.append('%s line %d: the first step can not start with %s' % (where, n, m.group(1)))
+ keyword = keyword if m.group(1) in ('And', 'But', '*') else m.group(1)
+ if fence:
+ errors.append(where + ': a doc string is not closed with ' + fence)
+ if need_then and not any(re.match(r'^Then\s', line.strip()) for line in lines):
+ errors.append(where + ': needs a Then step, the expected result')
+ return errors
+
+
+def parse(text):
+ """A .feature file as a test file dict. Raises ParseError."""
+ doc = {'title': '', 'tests': []}
+ tags, section, fence, indent, table = [], None, None, 0, None
+ lines = text.splitlines()
+
+ def fail(n, msg):
+ raise ParseError('line %d: %s' % (n, msg))
+
+ for n, raw in enumerate(lines, 1):
+ s = raw.strip()
+ if fence: # inside a doc string, kept as it is
+ section['steps'].append(raw[indent:] if raw[:indent].strip() == '' else s)
+ if s == fence:
+ section['steps'][-1] = s
+ fence = None
+ continue
+ if not s or s.startswith('#'):
+ continue
+ if s.startswith('@'):
+ for tag in s.split('#')[0].split():
+ if not TAG_RE.match(tag):
+ fail(n, 'bad tag ' + tag)
+ tags.append(tag[1:])
+ continue
+ table = table if s.startswith('|') else None
+ if s.startswith('Feature:'):
+ if 'feature' in doc:
+ fail(n, 'only one Feature per file')
+ doc['feature'] = True
+ doc['title'] = s[len('Feature:'):].strip()
+ if tags:
+ doc['tags'], tags = tags, []
+ section = doc
+ continue
+ if 'feature' not in doc:
+ fail(n, 'the file must start with Feature:')
+ if s.startswith('Background:'):
+ if doc['tests'] or 'background' in doc:
+ fail(n, 'the Background must come before the scenarios, once')
+ doc['background'] = []
+ section = {'steps': doc['background']}
+ continue
+ m = SCENARIO_RE.match(s)
+ if m:
+ section = scenario(m.group(2).strip(), tags)
+ doc['tests'].append(section)
+ tags = []
+ continue
+ if s.startswith(('Rule:', 'Scenarios:')) or (s.startswith('Examples:') and section is doc):
+ fail(n, s.split(':')[0] + ' is not supported here')
+ if s.startswith('Examples:'):
+ if 'name' not in section:
+ fail(n, 'Examples only belong to a scenario')
+ section['examples'] = section.get('examples', [])
+ table = []
+ continue
+ if table is not None: # a row of Examples
+ cells = [c.strip() for c in s.strip('|').split('|')]
+ if not s.endswith('|') or len(s) < 2:
+ fail(n, 'a table row must start and end with |')
+ if not table:
+ table.append(cells)
+ section['_columns'] = cells
+ elif len(cells) != len(table[0]):
+ fail(n, 'the row has %d cells, the header has %d' % (len(cells), len(table[0])))
+ else:
+ section['examples'].append(dict(zip(table[0], cells)))
+ continue
+ if section is doc:
+ doc.setdefault('description', []).append(s)
+ continue
+ if 'examples' in section:
+ fail(n, 'steps must come before the Examples')
+ if not section['steps'] and not STEP_RE.match(s) and 'name' in section:
+ if s.startswith('TODO:'): # description lines of a scenario
+ section['todo'] = (section.get('todo', '') + '\n' + s[5:].strip()).strip()
+ else:
+ section.setdefault('description', []).append(s)
+ continue
+ section['steps'].append(s)
+ if s.startswith(DOCSTRING):
+ fence, indent = s[:3], len(raw) - len(raw.lstrip())
+ if fence:
+ fail(len(lines), 'the doc string is not closed')
+ if 'feature' not in doc:
+ fail(1, 'no Feature: in the file')
+ del doc['feature']
+ for t in doc['tests']:
+ if 'examples' in t and not t['examples']:
+ raise ParseError('%s: Examples without rows' % t['name'])
+ t.pop('_columns', None)
+ return doc
+
+
+def scenario(name, tags):
+ t = {'name': name, 'steps': []}
+ other = []
+ for tag in tags:
+ key, _, value = tag.partition(':')
+ if key == 'id' and value:
+ t['id'] = value
+ elif key == 'ticket' and value:
+ t['ticket'] = '#' + value if value.isdigit() else value
+ elif key == 'project' and value:
+ t.setdefault('projects', []).append(value)
+ elif key == 'edition' and value:
+ t.setdefault('editions', []).append(value)
+ elif tag == 'disabled':
+ t['enabled'] = False
+ else:
+ other.append(tag)
+ if other:
+ t['tags'] = other
+ return t
+
+
+def write_steps(out, lines, indent):
+ """Doc strings and tables are indented below their step."""
+ fence = None
+ for line in lines:
+ s = line.strip()
+ inner = fence or s.startswith(DOCSTRING) or s.startswith('|')
+ out.append((indent + ' ' * bool(inner) + line) if line else '')
+ if fence and s == fence:
+ fence = None
+ elif not fence and s.startswith(DOCSTRING):
+ fence = s[:3]
+
+
+def table(rows):
+ cols = list(rows[0])
+ width = {c: max(len(c), *(len(r.get(c, '')) for r in rows)) for c in cols}
+ fmt = lambda cells: '| ' + ' | '.join(v.ljust(width[c]) for c, v in zip(cols, cells)) + ' |'
+ return [fmt(cols)] + [fmt([r.get(c, '') for c in cols]) for r in rows]
+
+
+def to_gherkin(doc):
+ out = []
+ if doc.get('tags'):
+ out.append(' '.join('@' + t for t in doc['tags']))
+ out.append('Feature: ' + doc.get('title', ''))
+ for line in doc.get('description', []):
+ out.append(' ' + line)
+ if doc.get('background'):
+ out += ['', ' Background:']
+ write_steps(out, doc['background'], ' ')
+ for t in doc['tests']:
+ tags = ['id:' + t['id']] if t.get('id') else []
+ if t.get('ticket'):
+ tags.append('ticket:' + t['ticket'].lstrip('#'))
+ tags += ['project:' + p for p in t.get('projects', [])]
+ if t.get('editions') and len(t['editions']) < len(EDITIONS):
+ tags += ['edition:' + e for e in t['editions']]
+ if t.get('enabled') is False:
+ tags.append('disabled')
+ tags += t.get('tags', [])
+ out.append('')
+ if tags:
+ out.append(' ' + ' '.join('@' + x for x in tags))
+ out.append(' %s: %s' % ('Scenario Outline' if t.get('examples') else 'Scenario', t['name']))
+ for line in t.get('description', []):
+ out.append(' ' + line)
+ for line in (t.get('todo') or '').splitlines():
+ out.append(' TODO: ' + line.strip())
+ write_steps(out, t.get('steps', []), ' ')
+ if t.get('examples'):
+ out += ['', ' Examples:']
+ out += [' ' + row for row in table(t['examples'])]
+ return '\n'.join(out) + '\n'
+
+
+def legacy_steps(steps, expected):
+ """Steps and expected result in plain words (markdown and older JSON files) as Gherkin lines."""
+ lines = []
+ for s in steps:
+ if s.startswith(('* ', '- ')) and lines:
+ lines[-1] += ' ' + s[2:]
+ else:
+ lines.append(('Given ' if not lines else 'When ' if len(lines) == 1 else 'And ') + s)
+ if expected:
+ lines.append('Then ' + ' '.join(expected.split()))
+ return lines
+
+
+def from_legacy_json(old):
+ """An older JSON test file: steps, expected and a matrix of axes."""
+ doc = {'title': old.get('title', ''), 'tests': []}
+ for o in old.get('tests', []):
+ t = {k: o[k] for k in ('id', 'name', 'ticket', 'enabled', 'todo', 'projects', 'editions', 'script', 'script_for')
+ if k in o}
+ matrix = o.get('matrix') or {}
+ steps = legacy_steps(o.get('steps', []), o.get('expected', ''))
+ t['steps'] = [re.sub(r'\{(%s)\}' % '|'.join(map(re.escape, matrix)), r'<\1>', s) if matrix else s for s in steps]
+ if matrix:
+ t['examples'] = [dict(zip(matrix, v)) for v in itertools.product(*matrix.values())]
+ doc['tests'].append(t)
+ return doc
+
+
+def from_markdown(text):
+ m = re.search(r'^#\s+(.+)$', text, re.M)
+ doc = {'title': m.group(1).strip() if m else '', 'tests': []}
+ for m in re.finditer(r'^## Test:\s*(.+?)\s*\n(.*?)(?=^## |\Z)', text, re.S | re.M):
+ test = {'name': m.group(1), 'steps': [], 'expected': '', 'todo': ''}
+ field = None
+ for line in m.group(2).splitlines():
+ s = line.strip()
+ if s.startswith('Ticket:'):
+ test['ticket'] = s[len('Ticket:'):].strip()
+ field = None
+ elif s.startswith('EXPECTED:'):
+ test['expected'] = s[len('EXPECTED:'):].strip()
+ field = 'expected'
+ elif s.startswith('TODO:'):
+ test['todo'] = s[len('TODO:'):].strip()
+ field = 'todo'
+ elif not s:
+ field = None
+ elif field:
+ test[field] += '\n' + s
+ else:
+ test['steps'].append(re.sub(r'^\d+\.\s+', '', s))
+ test['steps'] = legacy_steps(test['steps'], test.pop('expected'))
+ if not test['todo']:
+ del test['todo']
+ doc['tests'].append(test)
+ return doc
+
+
+def validate(doc, root=None):
+ """Return a list of error strings, empty when the document is fine. With ROOT (the directory of
+ the test file) the test projects must exist."""
+ errors = []
+ if not isinstance(doc, dict) or not isinstance(doc.get('tests'), list):
+ return ['the document needs a "tests" list']
+ if not isinstance(doc.get('title', ''), str) or '\n' in doc.get('title', ''):
+ errors.append('the title must be one line')
+ background = doc.get('background', [])
+ if not isinstance(background, list) or not all(isinstance(s, str) for s in background):
+ errors.append('the background must be a list of lines')
+ else:
+ errors += validate_steps(background, 'Background', need_then=False)
+ names = set()
+ for i, t in enumerate(doc['tests'], 1):
+ where = 'test %d' % i
+ if not isinstance(t, dict):
+ errors.append(where + ': not an object')
+ continue
+ name = t.get('name')
+ if not isinstance(name, str) or not name.strip():
+ errors.append(where + ': name is missing')
+ elif '\n' in name:
+ errors.append(where + ': the name must be one line')
+ elif name in names:
+ errors.append('%s: duplicate name "%s"' % (where, name))
+ else:
+ names.add(name)
+ steps = t.get('steps', [])
+ if not isinstance(steps, list) or not all(isinstance(s, str) and '\n' not in s for s in steps):
+ errors.append(where + ': steps must be a list of lines')
+ else:
+ errors += validate_steps(steps, where)
+ for k in ('todo', 'ticket', 'id'):
+ if not isinstance(t.get(k, ''), str):
+ errors.append('%s: %s must be a string' % (where, k))
+ if re.search(r'\s', t.get('ticket', '') or '') or re.search(r'\W', t.get('id', '') or ''):
+ errors.append(where + ': the ticket and the id can not have spaces, they are tags')
+ projects = t.get('projects', [])
+ if not isinstance(projects, list) or not all(isinstance(p, str) and PROJECT_RE.match(p) for p in projects):
+ errors.append(where + ': projects must be a list of project names (letters, digits, _ and -)')
+ elif root:
+ errors += ['%s: the test project "%s" does not exist' % (where, p) for p in projects
+ if not os.path.isdir(os.path.join(root, PROJECTS, p))]
+ editions = t.get('editions', list(EDITIONS))
+ if (not isinstance(editions, list) or not editions or len(set(editions)) != len(editions)
+ or not all(e in EDITIONS for e in editions)):
+ errors.append('%s: editions must be a list of one or more of %s' % (where, ', '.join(EDITIONS)))
+ errors += validate_script(t.get('script', []), where + ' script')
+ examples = t.get('examples', [])
+ if not isinstance(examples, list) or not all(isinstance(r, dict) for r in examples):
+ errors.append(where + ': examples must be a list of rows')
+ continue
+ if examples:
+ cols = list(examples[0])
+ for c in cols:
+ if not AXIS_RE.match(c):
+ errors.append('%s: Examples column "%s" must be letters, digits and _' % (where, c))
+ if any(list(r) != cols for r in examples):
+ errors.append(where + ': every row of the Examples must have the same columns')
+ if not all(isinstance(v, str) and v and '|' not in v for r in examples for v in r.values()):
+ errors.append(where + ': every cell of the Examples needs a value without |')
+ if len({json.dumps(r, sort_keys=True) for r in examples}) != len(examples):
+ errors.append(where + ': a row of the Examples is there twice')
+ return errors
+
+
+def scripts_path(path):
+ return path[:-len(EXT)] + SCRIPTS_EXT if path.endswith(EXT) else path + SCRIPTS_EXT
+
+
+def load(path):
+ with open(path, encoding='utf-8') as f:
+ text = f.read()
+ if path.endswith('.md'):
+ return from_markdown(text)
+ if path.endswith('.json'):
+ return from_legacy_json(json.loads(text))
+ doc = parse(text)
+ try:
+ with open(scripts_path(path), encoding='utf-8') as f:
+ scripts = json.load(f)
+ except FileNotFoundError:
+ scripts = {}
+ for t in doc['tests']:
+ s = scripts.get(t.get('id')) if t.get('id') else None
+ if isinstance(s, dict) and s.get('script'):
+ t['script'] = s['script']
+ if s.get('script_for'):
+ t['script_for'] = s['script_for']
+ return doc
+
+
+def combinations(test):
+ """The rows of the Examples of a test as a list of {column: value}; [{}] when there are none."""
+ return [dict(r) for r in test.get('examples') or []] or [{}]
+
+
+def editions(test):
+ """The editions that a test is for, all of them when it does not say."""
+ return test.get('editions') or list(EDITIONS)
+
+
+def substitute(text, combo):
+ return re.sub(r'\{([\w:-]+)\}', lambda m: combo.get(m.group(1), m.group(0)), text)
+
+
+def substitute_step(text, combo):
+ """ of the Examples and {edition} in a line of the steps."""
+ text = re.sub(r'<(\w+)>', lambda m: combo.get(m.group(1), m.group(0)), text)
+ return substitute(text, {k: v for k, v in combo.items() if k == 'edition'})
+
+
+def body(test, combo, edition=None, background=()):
+ """The test as it is shown to Claude: the scenario in Gherkin, with the values of the run."""
+ parts = []
+ if test.get('ticket'):
+ parts.append('Ticket: ' + test['ticket'])
+ if edition:
+ parts.append('Edition of the application: ' + edition)
+ if combo:
+ parts.append('Examples row for this run: ' + ', '.join('%s=%s' % kv for kv in combo.items()))
+ combo = dict(combo, edition=edition) if edition else combo
+ lines = []
+ if background:
+ lines += ['Background:'] + [' ' + substitute_step(s, combo) for s in background] + ['']
+ lines += ['Scenario: ' + test['name']] + [' ' + substitute_step(s, combo) for s in test.get('steps', [])]
+ parts.append('\n'.join(lines))
+ if test.get('todo'):
+ parts.append('TODO: ' + substitute_step(test['todo'], combo))
+ return '\n\n'.join(parts)
+
+
+def project_files(root, name):
+ """{relative path: bytes} of a test project."""
+ base = os.path.join(root, PROJECTS, name)
+ out = {}
+ for dirpath, _, files in os.walk(base):
+ for f in files:
+ p = os.path.join(dirpath, f)
+ with open(p, 'rb') as fh:
+ out[os.path.relpath(p, base)] = fh.read()
+ return out
+
+
+def project_hash(root, name):
+ h = hashlib.sha1()
+ for path, data in sorted(project_files(root, name).items()):
+ h.update(path.encode() + b'\0' + data + b'\0')
+ return h.hexdigest()[:12]
+
+
+def materialize(root, names, work):
+ """Copy the test projects to WORK/projects/NAME. Returns the variables for the script."""
+ variables = {}
+ for name in names:
+ src = os.path.join(root, PROJECTS, name)
+ if not os.path.isdir(src):
+ raise FileNotFoundError('the test project "%s" does not exist' % name)
+ dst = os.path.join(work, PROJECTS, name)
+ shutil.copytree(src, dst)
+ variables['project:' + name] = dst
+ if len(names) == 1:
+ variables['project'] = variables['project:' + names[0]]
+ return variables
+
+
+def definition_hash(test, root=None, background=()):
+ """What a script depends on: changing any of these, the Background of the file or a file of a
+ test project makes the script outdated."""
+ d = {'steps': list(background or []) + list(test.get('steps') or []), 'todo': test.get('todo') or '',
+ 'examples': test.get('examples') or []}
+ if test.get('projects'):
+ d['projects'] = {p: project_hash(root, p) if root and os.path.isdir(os.path.join(root, PROJECTS, p)) else 'missing'
+ for p in test['projects']}
+ return hashlib.sha1(json.dumps(d, sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:12]
+
+
+def ensure_ids(doc):
+ for t in doc.get('tests', []):
+ if not t.get('id'):
+ t['id'] = 't' + hashlib.sha1(str(t.get('name', '')).encode()).hexdigest()[:8]
+ return doc
+
+
+def is_stale(test, root=None, background=()):
+ """A script that was made for another version of the test. No script_for: old file, trusted."""
+ return bool(test.get('script') and test.get('script_for')
+ and test['script_for'] != definition_hash(test, root, background))
+
+
+def stamp_scripts(doc, old, root=None):
+ """Before saving. A new, edited or approved ("_approve") script is current for the test as it is
+ now. An untouched script keeps what it was made for, so editing the test makes it outdated."""
+ ensure_ids(doc)
+ old = ensure_ids(old or {})
+ prev = {t['id']: t for t in old.get('tests', [])}
+ for t in doc['tests']:
+ t.pop('_stale', None)
+ approve = t.pop('_approve', False)
+ if not t.get('script'):
+ t.pop('script_for', None)
+ continue
+ p = prev.get(t['id'])
+ if approve or p is None or p.get('script') != t['script']:
+ t['script_for'] = definition_hash(t, root, doc.get('background'))
+ else:
+ t['script_for'] = p.get('script_for') or definition_hash(p, root, old.get('background'))
+ return doc
+
+
+def subst_deep(obj, combo):
+ if isinstance(obj, str):
+ return substitute(obj, combo)
+ if isinstance(obj, list):
+ return [subst_deep(x, combo) for x in obj]
+ if isinstance(obj, dict):
+ return {k: subst_deep(v, combo) for k, v in obj.items()}
+ return obj
+
+
+def expand(path, edition=None):
+ """One dict {name, id, body, script, stale, combo} per Examples row of every enabled test.
+ With EDITION only the tests for that edition, and {edition} is replaced."""
+ doc = ensure_ids(load(path))
+ root = os.path.dirname(os.path.abspath(path))
+ errors = validate(doc, root)
+ if errors:
+ sys.exit('error: %s: %s' % (path, '; '.join(errors)))
+ background = doc.get('background', [])
+ out = []
+ for t in doc['tests']:
+ if not t.get('enabled', True) or edition and edition not in editions(t):
+ continue
+ for combo in combinations(t):
+ name = t['name'] + (' [%s]' % ', '.join('%s=%s' % kv for kv in combo.items()) if combo else '')
+ values = dict(combo, edition=edition) if edition else combo
+ out.append({'name': name, 'id': t['id'], 'body': body(t, combo, edition, background), 'combo': combo,
+ 'root': root, 'projects': t.get('projects', []), 'stale': is_stale(t, root, background),
+ 'script': subst_deep(t.get('script', []), values)})
+ return out
+
+
+def write_atomic(path, text):
+ tmp = path + '.tmp'
+ with open(tmp, 'w', encoding='utf-8') as f:
+ f.write(text)
+ os.replace(tmp, path)
+
+
+def save(path, doc):
+ """Write FILE.feature and the scripts in FILE.scripts.json."""
+ if not path.endswith(EXT):
+ raise ValueError('a test file is saved as ' + EXT)
+ ensure_ids(doc)
+ scripts = {t['id']: {k: t[k] for k in ('script', 'script_for') if t.get(k)} for t in doc['tests'] if t.get('script')}
+ write_atomic(path, to_gherkin(doc))
+ if scripts:
+ write_atomic(scripts_path(path), json.dumps(scripts, indent=2, ensure_ascii=False) + '\n')
+ elif os.path.exists(scripts_path(path)):
+ os.remove(scripts_path(path))
+
+
+def convert(src, dst):
+ """Import a markdown or an older JSON test file. Scripts that were up to date stay up to date."""
+ root = os.path.dirname(os.path.abspath(src))
+ current = set()
+ if src.endswith('.json'):
+ with open(src, encoding='utf-8') as f:
+ old = json.load(f)
+ for o in old.get('tests', []):
+ d = {k: o.get(k) or ([] if k == 'steps' else {} if k == 'matrix' else '') for k in ('steps', 'expected', 'todo', 'matrix')}
+ if o.get('projects'):
+ d['projects'] = {p: project_hash(root, p) if os.path.isdir(os.path.join(root, PROJECTS, p)) else 'missing'
+ for p in o['projects']}
+ h = hashlib.sha1(json.dumps(d, sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:12]
+ if o.get('script') and o.get('script_for') in (None, h):
+ current.add(o.get('id'))
+ doc = ensure_ids(load(src))
+ for t in doc['tests']:
+ if t['id'] in current:
+ t['script_for'] = definition_hash(t, os.path.dirname(os.path.abspath(dst)), doc.get('background'))
+ save(dst, doc)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 4 or sys.argv[1] != 'convert' or not sys.argv[3].endswith(EXT):
+ sys.exit(__doc__)
+ convert(sys.argv[2], sys.argv[3])
diff --git a/gui/manualtest/tests/analysis.feature b/gui/manualtest/tests/analysis.feature
new file mode 100644
index 00000000000..fc43506dd98
--- /dev/null
+++ b/gui/manualtest/tests/analysis.feature
@@ -0,0 +1,55 @@
+Feature: Analysis and recheck
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:te65b1fa8 @project:basic-project
+ Scenario: Reanalyze all files
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I change {project:basic-project}/src/a.c and {project:basic-project}/src/b.c so that they only contain ";"
+ And I click Reanalyze all files
+ And I wait until the analysis is done
+ Then there are no results for a.c
+ And there are no results for b.c
+
+ @id:tc707b85b @ticket:14893 @project:recheck
+ Scenario: Recheck a changed file
+ Given the project file {project:recheck}/p.cppcheck is opened
+ And the analysis is done
+ And the results show a nullPointer error in a.c and an arrayIndexOutOfBounds error in b.c
+ When I copy {project:recheck}/a_v2.c over {project:recheck}/a.c (a_v2.c has an uninitvar error instead)
+ And I right click on the file a.c in the results and click Recheck
+ Then the results for a.c are refreshed
+ And the nullPointer error in a.c is gone
+ And the uninitvar error from the changed a.c is shown
+ And the arrayIndexOutOfBounds error in b.c is still shown, unchanged
+
+ @id:tc550387d @project:basic-project
+ Scenario: Recheck a header file
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I right click on the file util.h in the results
+ Then the context menu of util.h has a Recheck entry
+ When I click Recheck
+ And I wait until the analysis is done
+ Then the analysis is run again for the files that include util.h
+ And the nullPointer error in util.h is shown again
+
+ @id:ta6f3b40b @project:syntax-error
+ Scenario Outline: Analyze a broken source file
+ Given the project file {project:syntax-error}/.cppcheck checks .c, a broken or unusual source file, and ok.c, which has a nullPointer error
+ When I open the project file {project:syntax-error}/.cppcheck
+ And I wait until the analysis is done
+ Then the analysis finishes
+ And the application does not crash
+ And the nullPointer error in ok.c is shown
+
+ Examples:
+ | case |
+ | braces |
+ | empty |
+ | binary |
+ | latin1 |
+ | long_line |
+ | unclosed_nesting |
diff --git a/gui/manualtest/tests/analysis.scripts.json b/gui/manualtest/tests/analysis.scripts.json
new file mode 100644
index 00000000000..54d89a4c39d
--- /dev/null
+++ b/gui/manualtest/tests/analysis.scripts.json
@@ -0,0 +1,399 @@
+{
+ "te65b1fa8": {
+ "script": [
+ {
+ "label": "Open the File menu",
+ "do": "click",
+ "name": "File",
+ "role": "menu item"
+ },
+ {
+ "label": "Choose Open Project File",
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item"
+ },
+ {
+ "label": "Enter the project file path",
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck"
+ },
+ {
+ "label": "Confirm the file dialog",
+ "do": "key",
+ "keys": [
+ "Return"
+ ]
+ },
+ {
+ "label": "Wait until the first analysis shows results for a.c",
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 30
+ },
+ {
+ "label": "Results for b.c are shown too",
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell"
+ },
+ {
+ "label": "Edit a.c so it only contains ;",
+ "do": "write",
+ "path": "{project:basic-project}/src/a.c",
+ "content": ";"
+ },
+ {
+ "label": "Edit b.c so it only contains ;",
+ "do": "write",
+ "path": "{project:basic-project}/src/b.c",
+ "content": ";"
+ },
+ {
+ "label": "Click Reanalyze all files",
+ "do": "click",
+ "name": "Reanalyze all files",
+ "role": "button"
+ },
+ {
+ "label": "Wait until the analysis is done (No errors found dialog)",
+ "expect": "visible",
+ "name": "No errors found",
+ "role": "label",
+ "timeout": 30
+ },
+ {
+ "label": "Close the dialog",
+ "do": "click",
+ "name": "OK",
+ "role": "button"
+ },
+ {
+ "label": "No results for a.c",
+ "expect": "gone",
+ "name": "src/a.c",
+ "role": "table cell"
+ },
+ {
+ "label": "No results for b.c",
+ "expect": "gone",
+ "name": "src/b.c",
+ "role": "table cell"
+ }
+ ],
+ "script_for": "c692e526a6cd"
+ },
+ "tc707b85b": {
+ "script": [
+ {
+ "do": "wait",
+ "name": "Cppcheck",
+ "role": "frame",
+ "timeout": 20,
+ "label": "Application started with no project"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "button",
+ "label": "Open the project file dialog"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:recheck}/p.cppcheck",
+ "label": "Enter the path of p.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project"
+ },
+ {
+ "expect": "visible",
+ "name": "b.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis done, b.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "a.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "a.c in the results"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Let the analysis finish"
+ },
+ {
+ "do": "copy",
+ "from": "{project:recheck}/a_v2.c",
+ "to": "{project:recheck}/a.c",
+ "label": "Change a.c by copying a_v2.c over it"
+ },
+ {
+ "do": "rightclick",
+ "name": "a.c",
+ "role": "table cell",
+ "nth": 0,
+ "label": "Right click on a.c in the results"
+ },
+ {
+ "expect": "ocr",
+ "contains": "Recheck",
+ "label": "Context menu shows Recheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Return"
+ ],
+ "label": "Click Recheck"
+ },
+ {
+ "do": "sleep",
+ "seconds": 4,
+ "label": "Wait for the recheck"
+ },
+ {
+ "do": "click",
+ "name": "b.c",
+ "role": "table cell",
+ "nth": 0,
+ "label": "Select the b.c row"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right",
+ "Down",
+ "Down",
+ "Down",
+ "Right"
+ ],
+ "label": "Expand b.c and a.c to show their results"
+ },
+ {
+ "expect": "text",
+ "contains": "uninitvar",
+ "label": "The uninitvar error of the changed a.c is shown"
+ },
+ {
+ "expect": "text",
+ "not_contains": "nullPointer",
+ "label": "The nullPointer error is gone"
+ },
+ {
+ "expect": "text",
+ "contains": "arrayIndexOutOfBounds",
+ "label": "The arrayIndexOutOfBounds error in b.c is still shown"
+ }
+ ],
+ "script_for": "0c160519508a"
+ },
+ "tc550387d": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "include/util.h",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis is done and util.h is in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis finished for all files"
+ },
+ {
+ "do": "rightclick",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "Right click on util.h in the results"
+ },
+ {
+ "do": "sleep",
+ "seconds": 1,
+ "label": "Wait for the context menu"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Return"
+ ],
+ "label": "Click the Recheck entry of the context menu"
+ },
+ {
+ "expect": "text",
+ "contains": "Checking /tmp",
+ "timeout": 30,
+ "label": "The analysis was run again"
+ },
+ {
+ "expect": "text",
+ "contains": "src/a.c ...",
+ "timeout": 30,
+ "label": "The file that includes util.h is checked again"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Wait until the analysis is done"
+ },
+ {
+ "do": "click",
+ "name": "include/util.h",
+ "role": "table cell",
+ "mouse": true,
+ "label": "Select util.h with the mouse so it has focus"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand util.h"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "timeout": 10,
+ "label": "The nullPointer error in util.h is shown again"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference",
+ "role": "table cell",
+ "timeout": 5,
+ "label": "The nullPointer message is shown"
+ }
+ ],
+ "script_for": "dc8fcd30e236"
+ },
+ "ta6f3b40b": {
+ "script": [
+ {
+ "label": "Start the application with no project loaded",
+ "expect": "visible",
+ "name": "Cppcheck",
+ "role": "frame"
+ },
+ {
+ "label": "Open the project file",
+ "do": "click",
+ "name": "Open Project File",
+ "role": "button"
+ },
+ {
+ "label": "Wait for the Select Project File dialog",
+ "do": "wait",
+ "name": "Select Project File",
+ "timeout": 10
+ },
+ {
+ "label": "Enter the project file {case}.cppcheck",
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:syntax-error}/{case}.cppcheck"
+ },
+ {
+ "label": "Confirm the file dialog",
+ "do": "key",
+ "keys": [
+ "Return"
+ ]
+ },
+ {
+ "label": "The project is loaded",
+ "expect": "visible",
+ "name": "Project: {case}.cppcheck",
+ "role": "frame",
+ "timeout": 15
+ },
+ {
+ "label": "Wait until the analysis is done (ok.c is checked)",
+ "expect": "text",
+ "contains": "Checking ok.c",
+ "timeout": 60
+ },
+ {
+ "label": "Give the analysis time to finish",
+ "do": "sleep",
+ "seconds": 3
+ },
+ {
+ "label": "The analysis finishes: Stop analysis is disabled again",
+ "expect": "state",
+ "name": "Stop analysis",
+ "role": "button",
+ "not_state": "enabled",
+ "timeout": 60
+ },
+ {
+ "label": "The analysis finishes: Reanalyze all files is enabled again",
+ "expect": "state",
+ "name": "Reanalyze all files",
+ "role": "button",
+ "state": "enabled",
+ "timeout": 30
+ },
+ {
+ "label": "The broken source file {case}.c was analyzed",
+ "expect": "text",
+ "contains": "Checking {case}.c"
+ },
+ {
+ "label": "The nullPointer error in ok.c is shown (ok.c has a result row)",
+ "expect": "visible",
+ "name": "ok.c",
+ "role": "table cell"
+ },
+ {
+ "label": "The application does not crash",
+ "expect": "alive"
+ }
+ ],
+ "script_for": "3090dfe1cf47"
+ }
+}
diff --git a/gui/manualtest/tests/application.feature b/gui/manualtest/tests/application.feature
new file mode 100644
index 00000000000..378e1a636cc
--- /dev/null
+++ b/gui/manualtest/tests/application.feature
@@ -0,0 +1,59 @@
+Feature: Application, menus and help
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t41f9102b
+ Scenario: All menus open
+ When I open each of the menus File, Edit, View, Analyze and Help in turn
+ And I look at its entries and close it with Escape
+ Then each of the menus File, Edit, View, Analyze and Help opens
+ And each menu shows at least one entry
+
+ @id:t1cc2be0f
+ Scenario: Show and hide toolbars
+ Given all entries of View > Toolbar are checked
+ When I uncheck the entries of View > Toolbar one at a time
+ Then a toolbar that is unchecked is no longer shown in the main window
+ And the toolbars that are still checked are shown
+
+ @id:t5fdeee58 @edition:opensource
+ Scenario: Same version in the title and in About
+ Given the title of the main window is "Cppcheck "
+ When I note the version in the title
+ And I open Help > About
+ Then the About dialog shows the same version as the title of the main window
+
+ @id:t90321d9c
+ Scenario: Help contents
+ When I open Help > Contents
+ Then a help window opens
+ And the help window has contents, it is not empty
+
+ @id:tc059b730
+ Scenario: Other Help menu entries
+ When I open each of the other entries of the Help menu in turn (About and the rest)
+ And I close each window with Escape or its Close button
+ Then every entry opens a dialog or window with text in it
+ And none of them is empty
+
+ @id:t5bf8f6ae @ticket:13982
+ Scenario: Scratch pad
+ When I open View > Scratch Pad
+ And I enter this code, one line each:
+ """
+ int main(void)
+ {
+ int *p = 0;
+ return *p;
+ }
+ """
+ And I click Check
+ Then the code is analysed
+ And a nullPointer error is shown
+ And the application does not crash
+
+ @id:t1a2285d8
+ Scenario: Quit from the File menu
+ When I open File > Quit
+ Then the application closes
diff --git a/gui/manualtest/tests/application.scripts.json b/gui/manualtest/tests/application.scripts.json
new file mode 100644
index 00000000000..98f5b20a42d
--- /dev/null
+++ b/gui/manualtest/tests/application.scripts.json
@@ -0,0 +1,548 @@
+{
+ "t41f9102b": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Open XML...",
+ "role": "menu item",
+ "label": "File menu shows entries"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Edit",
+ "role": "menu item",
+ "label": "Open the Edit menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Clear results",
+ "role": "menu item",
+ "label": "Edit menu shows entries"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the Edit menu"
+ },
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Toolbars",
+ "role": "menu item",
+ "label": "View menu shows entries"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the View menu"
+ },
+ {
+ "do": "click",
+ "name": "Analyze",
+ "role": "menu item",
+ "label": "Open the Analyze menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Files...",
+ "role": "menu item",
+ "label": "Analyze menu shows entries"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the Analyze menu"
+ },
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Contents",
+ "role": "menu item",
+ "label": "Help menu shows entries"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the Help menu"
+ }
+ ],
+ "script_for": "06424398d415"
+ },
+ "t1cc2be0f": {
+ "script": [
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open View menu"
+ },
+ {
+ "do": "click",
+ "name": "Toolbars",
+ "role": "menu item",
+ "label": "Open Toolbars submenu"
+ },
+ {
+ "expect": "state",
+ "name": "Standard",
+ "role": "menu item",
+ "state": "checked",
+ "label": "Standard toolbar entry is checked"
+ },
+ {
+ "expect": "state",
+ "name": "Categories",
+ "role": "menu item",
+ "state": "checked",
+ "label": "Categories toolbar entry is checked"
+ },
+ {
+ "expect": "state",
+ "name": "Filter",
+ "role": "menu item",
+ "state": "checked",
+ "label": "Filter toolbar entry is checked"
+ },
+ {
+ "do": "click",
+ "name": "Standard",
+ "role": "menu item",
+ "label": "Uncheck Standard toolbar"
+ },
+ {
+ "expect": "gone",
+ "name": "Standard",
+ "role": "tool bar",
+ "label": "Standard toolbar is no longer shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Categories",
+ "role": "tool bar",
+ "label": "Categories toolbar still shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Filter",
+ "role": "tool bar",
+ "label": "Filter toolbar still shown"
+ },
+ {
+ "do": "click",
+ "name": "Categories",
+ "role": "menu item",
+ "label": "Uncheck Categories toolbar"
+ },
+ {
+ "expect": "gone",
+ "name": "Categories",
+ "role": "tool bar",
+ "label": "Categories toolbar is no longer shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Filter",
+ "role": "tool bar",
+ "label": "Filter toolbar still shown"
+ },
+ {
+ "do": "click",
+ "name": "Filter",
+ "role": "menu item",
+ "label": "Uncheck Filter toolbar"
+ },
+ {
+ "expect": "gone",
+ "name": "Filter",
+ "role": "tool bar",
+ "label": "Filter toolbar is no longer shown"
+ }
+ ],
+ "script_for": "6a99d7841493"
+ },
+ "t5fdeee58": {
+ "script": [
+ {
+ "expect": "capture",
+ "name": "Cppcheck",
+ "role": "frame",
+ "regex": "'Cppcheck (?:Premium )?(\\d[^']*)'",
+ "as": "version",
+ "label": "Note the version in the title of the main window"
+ },
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "About...",
+ "role": "menu item",
+ "label": "Open Help > About"
+ },
+ {
+ "expect": "visible",
+ "name": "About Cppcheck",
+ "role": "dialog",
+ "label": "About dialog is shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Version {version}",
+ "role": "label",
+ "label": "About dialog shows the same version as the main window title ({version})"
+ }
+ ],
+ "script_for": "5dadee22b5ec"
+ },
+ "t90321d9c": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "Contents",
+ "role": "menu item",
+ "label": "Choose Help > Contents"
+ },
+ {
+ "expect": "visible",
+ "name": "Cppcheck GUI help",
+ "role": "dialog",
+ "label": "A help window opens",
+ "observed_fail": true
+ },
+ {
+ "expect": "text",
+ "contains": "Contents",
+ "label": "The help window has contents (it is not empty)"
+ },
+ {
+ "expect": "text",
+ "not_contains": "was not found",
+ "label": "No error that the help file was not found"
+ }
+ ],
+ "script_for": "7c97b218aa0f"
+ },
+ "tc059b730": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "License...",
+ "role": "menu item",
+ "label": "Open License"
+ },
+ {
+ "expect": "visible",
+ "name": "License",
+ "role": "dialog",
+ "label": "License dialog opens"
+ },
+ {
+ "expect": "text",
+ "contains": "GNU GENERAL PUBLIC LICENSE",
+ "label": "License dialog has text"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close License with Escape"
+ },
+ {
+ "expect": "gone",
+ "name": "License",
+ "label": "License dialog is closed"
+ },
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "Authors...",
+ "role": "menu item",
+ "label": "Open Authors"
+ },
+ {
+ "expect": "visible",
+ "name": "Authors",
+ "role": "dialog",
+ "label": "Authors dialog opens"
+ },
+ {
+ "expect": "text",
+ "contains": "The cppcheck team",
+ "label": "Authors dialog has text"
+ },
+ {
+ "do": "click",
+ "name": "Close",
+ "role": "button",
+ "label": "Close Authors with its Close button"
+ },
+ {
+ "expect": "gone",
+ "name": "Authors",
+ "role": "dialog",
+ "label": "Authors dialog is closed"
+ },
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "About...",
+ "role": "menu item",
+ "label": "Open About"
+ },
+ {
+ "expect": "visible",
+ "name": "About Cppcheck",
+ "role": "dialog",
+ "label": "About dialog opens"
+ },
+ {
+ "expect": "text",
+ "contains": "Cppcheck - A tool for static C/C++ code analysis.",
+ "label": "About dialog has text"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close About with Escape"
+ },
+ {
+ "expect": "gone",
+ "name": "About Cppcheck",
+ "role": "dialog",
+ "label": "About dialog is closed"
+ },
+ {
+ "do": "click",
+ "name": "Help",
+ "role": "menu item",
+ "label": "Open the Help menu"
+ },
+ {
+ "do": "click",
+ "name": "Contents",
+ "role": "menu item",
+ "label": "Open Contents"
+ },
+ {
+ "expect": "visible",
+ "name": "Cppcheck",
+ "role": "alert",
+ "label": "Contents shows a message"
+ },
+ {
+ "expect": "text",
+ "contains": "Helpfile",
+ "label": "Message has text"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the message with Escape"
+ },
+ {
+ "expect": "visible",
+ "name": "Cppcheck GUI help",
+ "role": "dialog",
+ "label": "Help window opens"
+ },
+ {
+ "expect": "text",
+ "contains": "Introduction",
+ "observed_fail": true,
+ "label": "Help window has text in it, not empty"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the help window with Escape"
+ }
+ ],
+ "script_for": "97511d2313db"
+ },
+ "t5bf8f6ae": {
+ "script": [
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu"
+ },
+ {
+ "do": "click",
+ "name": "Show Scratchpad",
+ "role": "menu item",
+ "label": "Open View > Scratch Pad"
+ },
+ {
+ "expect": "visible",
+ "name": "Scratchpad",
+ "role": "dialog",
+ "label": "Scratchpad dialog is shown"
+ },
+ {
+ "do": "focus",
+ "path": "1.1",
+ "label": "Focus the code editor of the scratchpad"
+ },
+ {
+ "do": "type",
+ "value": "int main(void)",
+ "label": "Type line 1"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "New line"
+ },
+ {
+ "do": "type",
+ "value": "{",
+ "label": "Type line 2"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "New line"
+ },
+ {
+ "do": "type",
+ "value": " int *p = 0;",
+ "label": "Type line 3"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "New line"
+ },
+ {
+ "do": "type",
+ "value": " return *p;",
+ "label": "Type line 4"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "New line"
+ },
+ {
+ "do": "type",
+ "value": "}",
+ "label": "Type line 5"
+ },
+ {
+ "do": "click",
+ "name": "Check",
+ "role": "button",
+ "label": "Click Check"
+ },
+ {
+ "expect": "text",
+ "contains": "nullPointer",
+ "timeout": 15,
+ "label": "A nullPointer error is shown"
+ },
+ {
+ "expect": "text",
+ "contains": "Null pointer dereference: p",
+ "label": "The null pointer message is shown"
+ },
+ {
+ "expect": "alive",
+ "label": "The application did not crash"
+ }
+ ],
+ "script_for": "30d9d26803fd"
+ },
+ "t1a2285d8": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Quit",
+ "role": "menu item",
+ "label": "Choose File > Quit"
+ },
+ {
+ "expect": "exited",
+ "label": "The application closes"
+ }
+ ],
+ "script_for": "bc31dd4069bf"
+ }
+}
diff --git a/gui/manualtest/tests/preferences.feature b/gui/manualtest/tests/preferences.feature
new file mode 100644
index 00000000000..70d0a82fa47
--- /dev/null
+++ b/gui/manualtest/tests/preferences.feature
@@ -0,0 +1,42 @@
+Feature: Preferences
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t232a9783
+ Scenario: Default number of threads
+ When I open Edit > Preferences
+ And I look at the General tab
+ Then the number of threads is 1
+
+ @id:tf5b5a7e1
+ Scenario: Number of threads cannot be 0
+ Given Edit > Preferences is open on the General tab
+ When I type 0 in the number of threads field
+ And I close the dialog with OK
+ And I open Edit > Preferences again
+ Then the field has not accepted 0
+ And the number of threads is at least 1, it is never 0
+
+ @id:t841cca6c @project:basic-project
+ Scenario: Analysis with 100 threads
+ Given the number of threads is set to 100 in Edit > Preferences and the dialog is closed with OK
+ When I open the project file {project:basic-project}/basic.cppcheck from the File menu
+ And I wait until the analysis is done
+ Then the application does not crash
+ And the analysis is done and the results are shown
+
+ @id:t28ce8afe @project:embedded @edition:opensource @disabled
+ Scenario Outline: Misra rule text file
+ Given Edit > Preferences is open on the Addons tab
+ When I select the rule text file {project:embedded}/
+ And I close the dialog with OK
+ And I open the project file {project:embedded}/embedded.cppcheck
+ And I wait until the analysis is done
+ Then the Misra error for the unused parameter has the rule text from the file (TEST RULE TEXT: unused parameters are not allowed)
+ And there are no internalError results
+
+ Examples:
+ | rulefile |
+ | rules.txt |
+ | rules with spaces.txt |
diff --git a/gui/manualtest/tests/preferences.scripts.json b/gui/manualtest/tests/preferences.scripts.json
new file mode 100644
index 00000000000..2da801e7e71
--- /dev/null
+++ b/gui/manualtest/tests/preferences.scripts.json
@@ -0,0 +1,306 @@
+{
+ "t232a9783": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Edit",
+ "role": "menu item",
+ "label": "Open the Edit menu"
+ },
+ {
+ "do": "click",
+ "name": "Preferences",
+ "role": "menu item",
+ "label": "Open Edit > Preferences"
+ },
+ {
+ "expect": "visible",
+ "name": "Preferences",
+ "role": "dialog",
+ "label": "Preferences dialog is shown"
+ },
+ {
+ "expect": "state",
+ "name": "General",
+ "role": "page tab",
+ "state": "selected",
+ "label": "General tab is shown"
+ },
+ {
+ "expect": "value",
+ "name": "Number of threads",
+ "role": "text",
+ "equals": "1",
+ "label": "The number of threads is 1"
+ }
+ ],
+ "script_for": "3c6d225d76b6"
+ },
+ "tf5b5a7e1": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Edit",
+ "role": "menu item",
+ "label": "Open the Edit menu"
+ },
+ {
+ "do": "click",
+ "name": "Preferences",
+ "role": "menu item",
+ "label": "Open Edit > Preferences"
+ },
+ {
+ "expect": "visible",
+ "name": "Preferences",
+ "role": "dialog",
+ "label": "Preferences dialog is shown"
+ },
+ {
+ "do": "click",
+ "name": "General",
+ "role": "page tab",
+ "label": "Go to the General tab"
+ },
+ {
+ "do": "settext",
+ "name": "Number of threads",
+ "role": "text",
+ "value": "0",
+ "label": "Type 0 in the number of threads field"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Close the dialog with OK"
+ },
+ {
+ "do": "click",
+ "name": "Edit",
+ "role": "menu item",
+ "label": "Open the Edit menu again"
+ },
+ {
+ "do": "click",
+ "name": "Preferences",
+ "role": "menu item",
+ "label": "Open Edit > Preferences again"
+ },
+ {
+ "expect": "visible",
+ "name": "Preferences",
+ "role": "dialog",
+ "label": "Preferences dialog is shown again"
+ },
+ {
+ "do": "click",
+ "name": "General",
+ "role": "page tab",
+ "label": "Go to the General tab again"
+ },
+ {
+ "expect": "value",
+ "name": "Number of threads",
+ "role": "text",
+ "equals": "1",
+ "label": "The number of threads is at least 1, never 0"
+ }
+ ],
+ "script_for": "6a7660844e11"
+ },
+ "t841cca6c": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Edit",
+ "role": "menu item",
+ "label": "Open the Edit menu"
+ },
+ {
+ "do": "click",
+ "name": "Preferences",
+ "role": "menu item",
+ "label": "Open Preferences from the Edit menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Preferences",
+ "role": "dialog",
+ "label": "Preferences dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "Number of threads",
+ "role": "text",
+ "value": "100",
+ "label": "Set the number of threads to 100"
+ },
+ {
+ "expect": "value",
+ "name": "Number of threads",
+ "role": "text",
+ "equals": "100",
+ "label": "Thread count field holds 100"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Close the Preferences dialog with OK"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "expect": "visible",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "File dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the path of basic.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 60,
+ "label": "Analysis is done and results for src/b.c are shown"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Results for src/a.c are shown"
+ },
+ {
+ "expect": "text",
+ "contains": "Project: basic.cppcheck",
+ "label": "The project is loaded"
+ },
+ {
+ "expect": "alive",
+ "label": "The application did not crash"
+ }
+ ],
+ "script_for": "3f99ba3a159c"
+ },
+ "t28ce8afe": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Preferences",
+ "role": "button",
+ "label": "Open Preferences"
+ },
+ {
+ "do": "click",
+ "name": "Addons",
+ "role": "page tab",
+ "label": "Go to the Addons tab"
+ },
+ {
+ "do": "settext",
+ "name": "MISRA addon",
+ "role": "text",
+ "value": "{project:embedded}/{rulefile}",
+ "label": "Select the rule text file"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Close the dialog with OK"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:embedded}/embedded.cppcheck",
+ "label": "Enter the project file name"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "do": "sleep",
+ "seconds": 8,
+ "label": "Wait until the analysis is done"
+ },
+ {
+ "expect": "visible",
+ "name": "gpio.c",
+ "role": "table cell",
+ "timeout": 20,
+ "label": "Results for gpio.c are shown"
+ },
+ {
+ "do": "doubleclick",
+ "name": "gpio.c",
+ "role": "table cell",
+ "label": "Select the gpio.c result row"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand the result row"
+ },
+ {
+ "expect": "text",
+ "contains": "TEST RULE TEXT: unused parameters are not allowed",
+ "timeout": 10,
+ "label": "Misra error has the rule text from the file"
+ },
+ {
+ "expect": "text",
+ "contains": "misra-c2012-2.7",
+ "label": "The unused parameter Misra rule id is shown"
+ },
+ {
+ "expect": "text",
+ "not_contains": "internalError",
+ "label": "There are no internalError results"
+ }
+ ],
+ "script_for": "755194f793e0"
+ }
+}
diff --git a/gui/manualtest/tests/project_dialog.feature b/gui/manualtest/tests/project_dialog.feature
new file mode 100644
index 00000000000..2d28b7dbc0e
--- /dev/null
+++ b/gui/manualtest/tests/project_dialog.feature
@@ -0,0 +1,40 @@
+Feature: Project file dialog
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:tcf7ddb76 @ticket:14983 @project:relative-paths
+ Scenario: Relative paths in the saved project
+ Given a new project file is created and the project file dialog is open
+ When I import the project {project:relative-paths}/compile_commands.json (Paths and defines, Import project)
+ And I add the include directory {project:relative-paths}/inc
+ And I exclude the file {project:relative-paths}/skip/b.c
+ And I save the project file as {project:relative-paths}/rel.cppcheck
+ Then in the saved rel.cppcheck the import project is written relative to the project file: compile_commands.json
+ And the include directory is written relative to the project file: inc
+ And the excluded file is written relative to the project file: skip/b.c
+ And the XML contains no absolute path to the project folder
+
+ @id:t5afff002 @ticket:14489
+ Scenario: Select the platform pic8.xml
+ Given a new project file is created and the project file dialog is open
+ When I open the Platform drop-down list
+ And I select pic8.xml in the list
+ Then pic8.xml is in the Platform list
+ And pic8.xml is the platform that the dialog shows
+
+ @id:t91490557 @ticket:14672 @project:custom-cfg
+ Scenario: Custom cfg file in the library list
+ Given the folder of {project:custom-cfg}/p.cppcheck also holds aa.cfg, a copy of cfg/avr.cfg
+ When I open the project file {project:custom-cfg}/p.cppcheck
+ And I open the project file dialog (edit the project file)
+ And I look at the library list (Libraries)
+ Then aa is in the library list
+ And aa comes before avr, the list is in alphabetical order
+
+ @id:t0ec0680b @ticket:14488 @project:embedded @edition:opensource
+ Scenario: Misra checkbox of a loaded project
+ When I open the project file {project:embedded}/embedded.cppcheck
+ And I open the project file dialog (edit the project file)
+ And I go to the Addons tab
+ Then the Misra C 2012 checkbox is checked
diff --git a/gui/manualtest/tests/project_dialog.scripts.json b/gui/manualtest/tests/project_dialog.scripts.json
new file mode 100644
index 00000000000..08dfc5965ce
--- /dev/null
+++ b/gui/manualtest/tests/project_dialog.scripts.json
@@ -0,0 +1,371 @@
+{
+ "tcf7ddb76": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "New Project File",
+ "role": "menu item",
+ "label": "Create a new project file"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:relative-paths}/rel.cppcheck",
+ "label": "Enter the project file name"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the project file name"
+ },
+ {
+ "expect": "visible",
+ "name": "Project file: rel.cppcheck",
+ "role": "dialog",
+ "label": "Project file dialog is open"
+ },
+ {
+ "do": "click",
+ "name": "Add...",
+ "role": "button",
+ "nth": 1,
+ "label": "Add an include directory"
+ },
+ {
+ "do": "settext",
+ "name": "Directory:",
+ "role": "text",
+ "value": "{project:relative-paths}/inc",
+ "label": "Enter the include directory {project:relative-paths}/inc"
+ },
+ {
+ "do": "click",
+ "name": "Choose",
+ "role": "button",
+ "label": "Choose the include directory"
+ },
+ {
+ "do": "click",
+ "name": "Warning options",
+ "role": "page tab",
+ "label": "Open the tab with the excluded files"
+ },
+ {
+ "do": "click",
+ "name": "Exclude file",
+ "role": "button",
+ "label": "Exclude a file"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:relative-paths}/skip/b.c",
+ "label": "Enter the excluded file {project:relative-paths}/skip/b.c"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the excluded file"
+ },
+ {
+ "expect": "visible",
+ "name": "skip/b.c",
+ "role": "list item",
+ "label": "skip/b.c is in the exclude list"
+ },
+ {
+ "do": "click",
+ "name": "Paths and Defines",
+ "role": "page tab",
+ "label": "Back to Paths and Defines"
+ },
+ {
+ "do": "click",
+ "name": "Browse...",
+ "role": "button",
+ "nth": 0,
+ "label": "Browse for the import project"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:relative-paths}/compile_commands.json",
+ "label": "Enter the import project {project:relative-paths}/compile_commands.json"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the import project"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Save the project file as rel.cppcheck"
+ },
+ {
+ "do": "sleep",
+ "seconds": 1
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Dismiss the build dir question"
+ },
+ {
+ "expect": "file",
+ "path": "{project:relative-paths}/rel.cppcheck",
+ "contains": "compile_commands.json",
+ "label": "Import project is relative"
+ },
+ {
+ "expect": "file",
+ "path": "{project:relative-paths}/rel.cppcheck",
+ "contains": "",
+ "label": "Excluded file is relative (skip/b.c)"
+ },
+ {
+ "expect": "file",
+ "path": "{project:relative-paths}/rel.cppcheck",
+ "not_contains": "{project:relative-paths}",
+ "label": "No absolute path to the project folder in the XML"
+ }
+ ],
+ "script_for": "edfc96b05beb"
+ },
+ "t5afff002": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "New Project File",
+ "role": "menu item",
+ "label": "Create a new project file"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{work}/t.cppcheck",
+ "label": "Enter the project file name"
+ },
+ {
+ "do": "click",
+ "name": "Save",
+ "role": "button",
+ "exact": true,
+ "label": "Save the new project file; the project file dialog opens"
+ },
+ {
+ "expect": "visible",
+ "name": "Project file",
+ "role": "dialog",
+ "label": "Project file dialog is open"
+ },
+ {
+ "do": "click",
+ "name": "Types and Functions",
+ "role": "page tab",
+ "label": "Go to the tab with the Platform setting"
+ },
+ {
+ "do": "click",
+ "name": "Platform",
+ "role": "combo box",
+ "label": "Open the Platform drop-down list"
+ },
+ {
+ "expect": "visible",
+ "name": "pic8.xml",
+ "role": "list item",
+ "label": "pic8.xml is in the Platform list"
+ },
+ {
+ "do": "click",
+ "name": "pic8.xml",
+ "role": "list item",
+ "exact": true,
+ "mouse": true,
+ "label": "Select pic8.xml in the list"
+ },
+ {
+ "expect": "visible",
+ "name": "pic8.xml",
+ "role": "combo box",
+ "label": "The dialog shows pic8.xml as the platform"
+ }
+ ],
+ "script_for": "619f376b22e1"
+ },
+ "t91490557": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "button",
+ "label": "Open the project file dialog from the toolbar"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:custom-cfg}/p.cppcheck",
+ "label": "Enter the path of p.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "expect": "visible",
+ "name": "Cppcheck",
+ "role": "alert",
+ "label": "Analysis finishes and an alert appears"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Dismiss the alert"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Edit Project File",
+ "role": "menu item",
+ "label": "Open the project file dialog (edit the project file)"
+ },
+ {
+ "expect": "visible",
+ "name": "Project file: p.cppcheck",
+ "role": "dialog",
+ "label": "The project file dialog is open"
+ },
+ {
+ "expect": "visible",
+ "name": "aa",
+ "role": "list item",
+ "exact": true,
+ "label": "aa is in the Libraries list"
+ },
+ {
+ "expect": "visible",
+ "name": "avr",
+ "role": "list item",
+ "exact": true,
+ "label": "avr is in the Libraries list"
+ }
+ ],
+ "script_for": "b0bf245f7042"
+ },
+ "t0ec0680b": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "File dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:embedded}/embedded.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "do": "wait",
+ "name": "Project: embedded.cppcheck",
+ "role": "frame",
+ "label": "Project is loaded"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu again"
+ },
+ {
+ "do": "click",
+ "name": "Edit Project File...",
+ "role": "menu item",
+ "label": "Open the project file dialog"
+ },
+ {
+ "do": "wait",
+ "name": "Project file: embedded.cppcheck",
+ "role": "dialog",
+ "label": "Project file dialog is shown"
+ },
+ {
+ "do": "click",
+ "name": "Addons",
+ "role": "page tab",
+ "label": "Go to the Addons tab"
+ },
+ {
+ "expect": "state",
+ "name": "Misra C 2012",
+ "role": "check box",
+ "state": "checked",
+ "label": "The Misra C 2012 checkbox is checked"
+ }
+ ],
+ "script_for": "187bd0d82a72"
+ }
+}
diff --git a/gui/manualtest/tests/projects.feature b/gui/manualtest/tests/projects.feature
new file mode 100644
index 00000000000..33312524239
--- /dev/null
+++ b/gui/manualtest/tests/projects.feature
@@ -0,0 +1,72 @@
+Feature: Open and create projects
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:tf9fc8bf6 @project:basic-project
+ Scenario: Open a project file
+ When I open the project file {project:basic-project}/basic.cppcheck with the open project file entry in the File menu
+ Then the project opens
+ And the title of the main window names basic.cppcheck
+ And the analysis results are shown: nullPointer errors in a.c and util.h, arrayIndexOutOfBounds in b.c
+
+ @id:t6bb11424 @project:switch-project
+ Scenario: Open a second project
+ TODO: with and without build dir
+ Given the project file {project:switch-project}/one/p1.cppcheck is opened
+ And the analysis is done
+ And the results show a nullPointer error in a.c
+ When I open the project file {project:switch-project}/two/p2.cppcheck
+ And I wait until the analysis is done
+ Then the results of the first project (nullPointer in a.c) are gone
+ And the results of the second project (arrayIndexOutOfBounds in b.c) are shown
+
+ @id:t176fd17f @project:switch-project
+ Scenario: Open a recent project
+ Given the project file {project:switch-project}/one/p1.cppcheck is opened and analysed
+ And the project file {project:switch-project}/two/p2.cppcheck is opened and analysed
+ When I open the File menu
+ And I click p1.cppcheck in the list of recent projects
+ Then the project p1.cppcheck is opened
+ And the title of the main window names p1.cppcheck, not p2.cppcheck or another file
+
+ @id:t1e899c1c @project:basic-project
+ Scenario: New project file
+ When I open File > New Project File
+ And I enter the file name {project:basic-project}/OSgui.cppcheck
+ And I add the folder {project:basic-project}/src as a path to check in the project file dialog
+ And I save the project file without selecting an addon
+ And I wait until the analysis is done
+ Then the project file OSgui.cppcheck is saved without any addon in it
+ And the analysis is run
+ And the results are shown: nullPointer errors in a.c, arrayIndexOutOfBounds in b.c
+ And util.h is not checked, the new project has no include path to it
+
+ @id:t202b9515 @project:basic-project
+ Scenario Outline: New project file with an addon
+ When I open File > New Project File
+ And I enter the file name {project:basic-project}/OSgui.cppcheck
+ And I add the folder {project:basic-project}/src as a path to check in the project file dialog
+ And I tick the checkbox of the addon whose name contains "" on the Addons tab (Misra C, Y2038 or Thread safety)
+ And I save the project file
+ And I wait until the analysis is done
+ Then the project file OSgui.cppcheck is saved with an addon entry that contains "" (misra, y2038 or threadsafety)
+ And the analysis is run and results are shown
+ And none of the results is an internalError about the addon
+
+ Examples:
+ | addon |
+ | misra |
+ | y2038 |
+ | thread |
+
+ @id:tf6da2cfd @project:basic-project
+ Scenario: Add the Misra addon to a project
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I open File > Edit Project File
+ And I select MISRA C 2012 on the Addons tab
+ And I save the project file
+ And I wait until the analysis is done
+ Then basic.cppcheck has the addon misra in it
+ And the analysis is run again
diff --git a/gui/manualtest/tests/projects.scripts.json b/gui/manualtest/tests/projects.scripts.json
new file mode 100644
index 00000000000..a78d2f6b793
--- /dev/null
+++ b/gui/manualtest/tests/projects.scripts.json
@@ -0,0 +1,771 @@
+{
+ "tf9fc8bf6": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File in the File menu"
+ },
+ {
+ "expect": "visible",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "The project file dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the path of basic.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the dialog to open the project"
+ },
+ {
+ "expect": "visible",
+ "name": "Project: basic.cppcheck",
+ "role": "frame",
+ "timeout": 15,
+ "label": "The title of the main window names basic.cppcheck"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "gone": true,
+ "label": "The dialog is closed"
+ },
+ {
+ "do": "sleep",
+ "seconds": 4,
+ "label": "Wait for the analysis to finish"
+ },
+ {
+ "do": "focus",
+ "role": "tree",
+ "label": "Focus the results tree"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Home",
+ "Down",
+ "Down",
+ "Right",
+ "Up",
+ "Right",
+ "Up",
+ "Right",
+ "Home"
+ ],
+ "label": "Expand the results of all three files and go to the top"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "timeout": 10,
+ "label": "nullPointer errors are shown for util.h and a.c"
+ },
+ {
+ "expect": "text",
+ "contains": "src/a.c",
+ "label": "Results for a.c are shown"
+ },
+ {
+ "expect": "text",
+ "contains": "include/util.h",
+ "label": "Results for util.h are shown"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "End"
+ ],
+ "label": "Scroll to the results of b.c"
+ },
+ {
+ "expect": "visible",
+ "name": "arrayIndexOutOfBounds",
+ "role": "table cell",
+ "timeout": 10,
+ "label": "arrayIndexOutOfBounds error is shown for b.c"
+ }
+ ],
+ "script_for": "7128457516b3"
+ },
+ "t6bb11424": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "button",
+ "label": "Open the first project"
+ },
+ {
+ "do": "type",
+ "value": "{project:switch-project}/one/p1.cppcheck",
+ "label": "Enter path of p1.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "Project: p1.cppcheck",
+ "role": "frame",
+ "timeout": 10,
+ "label": "First project is loaded"
+ },
+ {
+ "expect": "visible",
+ "name": "a.c",
+ "role": "table cell",
+ "timeout": 20,
+ "label": "Results of the first project show a.c"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "button",
+ "label": "Open the second project"
+ },
+ {
+ "do": "type",
+ "value": "{project:switch-project}/two/p2.cppcheck",
+ "label": "Enter path of p2.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "Project: p2.cppcheck",
+ "role": "frame",
+ "timeout": 10,
+ "label": "Second project is loaded"
+ },
+ {
+ "expect": "visible",
+ "name": "b.c",
+ "role": "table cell",
+ "timeout": 20,
+ "label": "Results of the second project show b.c"
+ },
+ {
+ "expect": "gone",
+ "name": "a.c",
+ "role": "table cell",
+ "label": "Results of the first project are gone"
+ }
+ ],
+ "script_for": "70097c80a6ed"
+ },
+ "t176fd17f": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:switch-project}/one/p1.cppcheck",
+ "label": "Enter the path of p1.cppcheck"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "exact": true,
+ "label": "Open p1.cppcheck"
+ },
+ {
+ "expect": "text",
+ "contains": "Project: p1.cppcheck",
+ "timeout": 10,
+ "label": "Project p1 is open"
+ },
+ {
+ "expect": "visible",
+ "name": "Checking a.c",
+ "timeout": 15,
+ "label": "Analysis of p1 is done"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:switch-project}/two/p2.cppcheck",
+ "label": "Enter the path of p2.cppcheck"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "exact": true,
+ "label": "Open p2.cppcheck"
+ },
+ {
+ "expect": "text",
+ "contains": "Project: p2.cppcheck",
+ "timeout": 10,
+ "label": "Project p2 is open"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Wait until the analysis of p2 is done"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "p1.cppcheck",
+ "role": "menu item",
+ "label": "Click p1.cppcheck in the recent projects list"
+ },
+ {
+ "expect": "text",
+ "contains": "Project: p1.cppcheck",
+ "timeout": 10,
+ "label": "Main window title names p1.cppcheck"
+ },
+ {
+ "expect": "text",
+ "not_contains": "Project: p2.cppcheck",
+ "label": "Title does not name p2.cppcheck"
+ }
+ ],
+ "script_for": "2ea42621479d"
+ },
+ "t1e899c1c": {
+ "script": [
+ {
+ "label": "Open File menu",
+ "do": "click",
+ "name": "File",
+ "role": "menu item"
+ },
+ {
+ "label": "Open File > New Project File",
+ "do": "click",
+ "name": "New Project File",
+ "role": "menu item"
+ },
+ {
+ "label": "Wait for the file name dialog",
+ "do": "wait",
+ "name": "Select Project Filename",
+ "role": "dialog"
+ },
+ {
+ "label": "Enter the file name OSgui.cppcheck",
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/OSgui.cppcheck"
+ },
+ {
+ "label": "Confirm the file name",
+ "do": "key",
+ "keys": [
+ "Return"
+ ]
+ },
+ {
+ "label": "Project file dialog is shown",
+ "expect": "visible",
+ "name": "Project file: OSgui.cppcheck",
+ "role": "dialog"
+ },
+ {
+ "label": "Add a path to check",
+ "do": "click",
+ "name": "Add...",
+ "role": "button",
+ "nth": 0
+ },
+ {
+ "label": "Wait for the directory dialog",
+ "do": "wait",
+ "name": "Select a directory to check",
+ "role": "dialog"
+ },
+ {
+ "label": "Enter the src folder",
+ "do": "settext",
+ "name": "Directory:",
+ "role": "text",
+ "value": "{project:basic-project}/src"
+ },
+ {
+ "label": "Choose the src folder",
+ "do": "click",
+ "name": "Choose",
+ "role": "button"
+ },
+ {
+ "label": "src is in the paths list",
+ "expect": "visible",
+ "name": "src",
+ "role": "list item"
+ },
+ {
+ "label": "Save the project file without selecting an addon",
+ "do": "click",
+ "name": "OK",
+ "role": "button"
+ },
+ {
+ "label": "Create the build dir when asked",
+ "do": "click",
+ "name": "Yes",
+ "role": "button"
+ },
+ {
+ "label": "Wait until the analysis has checked b.c",
+ "expect": "text",
+ "contains": "Checking src/b.c",
+ "timeout": 30
+ },
+ {
+ "label": "Wait for the results of a.c",
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 30
+ },
+ {
+ "label": "Select a.c results",
+ "do": "click",
+ "name": "src/a.c",
+ "role": "table cell",
+ "nth": 0,
+ "mouse": true
+ },
+ {
+ "label": "Collapse a.c results to get a known state",
+ "do": "key",
+ "keys": [
+ "Left"
+ ]
+ },
+ {
+ "label": "Expand a.c results",
+ "do": "key",
+ "keys": [
+ "Right"
+ ]
+ },
+ {
+ "label": "nullPointer is reported in a.c",
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "timeout": 10
+ },
+ {
+ "label": "Select b.c results",
+ "do": "click",
+ "name": "src/b.c",
+ "role": "table cell",
+ "nth": 0,
+ "mouse": true
+ },
+ {
+ "label": "Collapse b.c results to get a known state",
+ "do": "key",
+ "keys": [
+ "Left"
+ ]
+ },
+ {
+ "label": "Expand b.c results",
+ "do": "key",
+ "keys": [
+ "Right"
+ ]
+ },
+ {
+ "label": "arrayIndexOutOfBounds is reported in b.c",
+ "expect": "visible",
+ "name": "arrayIndexOutOfBounds",
+ "role": "table cell",
+ "timeout": 10
+ },
+ {
+ "label": "util.h is not checked",
+ "expect": "text",
+ "not_contains": "Checking include/util.h"
+ },
+ {
+ "label": "Project file contains the src path",
+ "expect": "file",
+ "path": "{project:basic-project}/OSgui.cppcheck",
+ "contains": ""
+ },
+ {
+ "label": "Project file has no addon",
+ "expect": "file",
+ "path": "{project:basic-project}/OSgui.cppcheck",
+ "not_contains": "addon"
+ },
+ {
+ "label": "Project file has no include path",
+ "expect": "file",
+ "path": "{project:basic-project}/OSgui.cppcheck",
+ "not_contains": "include"
+ }
+ ],
+ "script_for": "b3616d866fea"
+ },
+ "t202b9515": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "New Project File...",
+ "role": "menu item",
+ "label": "Open File > New Project File"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project Filename",
+ "role": "dialog",
+ "label": "Wait for the file name dialog"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/OSgui.cppcheck",
+ "label": "Enter the file name OSgui.cppcheck"
+ },
+ {
+ "do": "focus",
+ "name": "File name:",
+ "role": "text",
+ "label": "Focus the file name field"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file name"
+ },
+ {
+ "do": "wait",
+ "name": "Project file: OSgui.cppcheck",
+ "role": "dialog",
+ "timeout": 10,
+ "label": "Wait for the project file dialog"
+ },
+ {
+ "do": "click",
+ "name": "Paths and Defines",
+ "role": "page tab",
+ "label": "Go to the Paths and Defines tab"
+ },
+ {
+ "do": "click",
+ "name": "Add...",
+ "role": "button",
+ "nth": 0,
+ "label": "Add a path to check"
+ },
+ {
+ "do": "wait",
+ "name": "Select a directory to check",
+ "role": "dialog",
+ "label": "Wait for the directory dialog"
+ },
+ {
+ "do": "settext",
+ "name": "Directory:",
+ "role": "text",
+ "value": "{project:basic-project}/src",
+ "label": "Enter the folder src"
+ },
+ {
+ "do": "click",
+ "name": "Choose",
+ "role": "button",
+ "label": "Choose the folder src"
+ },
+ {
+ "do": "wait",
+ "name": "Select a directory to check",
+ "role": "dialog",
+ "gone": true,
+ "label": "Directory dialog closes"
+ },
+ {
+ "expect": "visible",
+ "name": "src",
+ "role": "list item",
+ "exact": true,
+ "label": "src is in the paths to check"
+ },
+ {
+ "do": "click",
+ "name": "Addons",
+ "role": "page tab",
+ "label": "Go to the Addons tab"
+ },
+ {
+ "do": "click",
+ "name": "{addon}",
+ "role": "check box",
+ "nth": 0,
+ "label": "Tick the addon checkbox"
+ },
+ {
+ "expect": "state",
+ "name": "{addon}",
+ "role": "check box",
+ "nth": 0,
+ "state": "checked",
+ "label": "The addon checkbox is ticked"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Save the project file"
+ },
+ {
+ "do": "wait",
+ "name": "Yes",
+ "role": "button",
+ "timeout": 10,
+ "label": "Question to create the build dir"
+ },
+ {
+ "do": "click",
+ "name": "Yes",
+ "role": "button",
+ "label": "Create the build dir"
+ },
+ {
+ "do": "wait",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 120,
+ "label": "Wait for the analysis results"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3
+ },
+ {
+ "expect": "state",
+ "name": "Stop analysis",
+ "role": "button",
+ "not_state": "enabled",
+ "timeout": 120,
+ "label": "The analysis is done"
+ },
+ {
+ "do": "focus",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Select the results of a.c"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "asterisk",
+ "End",
+ "asterisk"
+ ],
+ "label": "Expand the results of all files"
+ },
+ {
+ "do": "sleep",
+ "seconds": 1
+ },
+ {
+ "expect": "file",
+ "path": "{project:basic-project}/OSgui.cppcheck",
+ "contains": "{addon}",
+ "label": "Project file has the addon entry"
+ },
+ {
+ "expect": "file",
+ "path": "{project:basic-project}/OSgui.cppcheck",
+ "contains": "",
+ "label": "Project file has the path src"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Analysis results are shown"
+ },
+ {
+ "expect": "text",
+ "not_contains": "internalError",
+ "label": "No internalError about the addon in the results"
+ }
+ ],
+ "script_for": "60cd34c48191"
+ },
+ "tf6da2cfd": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "do": "wait",
+ "name": "Checking src/b.c",
+ "timeout": 20,
+ "label": "Wait for the first analysis to run"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Let the first analysis finish"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Edit Project File",
+ "role": "menu item",
+ "label": "Choose Edit Project File"
+ },
+ {
+ "expect": "visible",
+ "name": "Project file: basic.cppcheck",
+ "role": "dialog",
+ "label": "Project file dialog is open"
+ },
+ {
+ "do": "click",
+ "name": "Addons",
+ "role": "page tab",
+ "label": "Go to the Addons tab"
+ },
+ {
+ "do": "click",
+ "name": "Misra C 2012",
+ "role": "check box",
+ "label": "Select MISRA C 2012"
+ },
+ {
+ "expect": "state",
+ "name": "Misra C 2012",
+ "state": "checked",
+ "label": "MISRA C 2012 is checked"
+ },
+ {
+ "do": "click",
+ "name": "OK",
+ "role": "button",
+ "label": "Save the project file"
+ },
+ {
+ "expect": "gone",
+ "name": "Project file: basic.cppcheck",
+ "label": "Project file dialog is closed"
+ },
+ {
+ "do": "sleep",
+ "seconds": 4,
+ "label": "Wait for the second analysis to finish"
+ },
+ {
+ "expect": "file",
+ "path": "{project:basic-project}/basic.cppcheck",
+ "contains": "misra",
+ "label": "Project file has the misra addon"
+ },
+ {
+ "expect": "visible",
+ "name": "Checking src/b.c",
+ "role": "list item",
+ "nth": 1,
+ "label": "The analysis was run again"
+ }
+ ],
+ "script_for": "452f351f0fb2"
+ }
+}
diff --git a/gui/manualtest/tests/projects/basic-project/basic.cppcheck b/gui/manualtest/tests/projects/basic-project/basic.cppcheck
new file mode 100644
index 00000000000..c8805a32587
--- /dev/null
+++ b/gui/manualtest/tests/projects/basic-project/basic.cppcheck
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/basic-project/include/util.h b/gui/manualtest/tests/projects/basic-project/include/util.h
new file mode 100644
index 00000000000..48ae2d54a95
--- /dev/null
+++ b/gui/manualtest/tests/projects/basic-project/include/util.h
@@ -0,0 +1,5 @@
+static inline int helper(void)
+{
+ int *r = 0;
+ return *r;
+}
diff --git a/gui/manualtest/tests/projects/basic-project/src/a.c b/gui/manualtest/tests/projects/basic-project/src/a.c
new file mode 100644
index 00000000000..3a84d86a5ff
--- /dev/null
+++ b/gui/manualtest/tests/projects/basic-project/src/a.c
@@ -0,0 +1,18 @@
+#include "util.h"
+
+int first(void)
+{
+ int *p = 0;
+ return *p;
+}
+
+int second(void)
+{
+ int *q = 0;
+ return *q;
+}
+
+int main(void)
+{
+ return first() + second() + helper();
+}
diff --git a/gui/manualtest/tests/projects/basic-project/src/b.c b/gui/manualtest/tests/projects/basic-project/src/b.c
new file mode 100644
index 00000000000..20be2edc145
--- /dev/null
+++ b/gui/manualtest/tests/projects/basic-project/src/b.c
@@ -0,0 +1,5 @@
+void f(void)
+{
+ char a[4];
+ a[10] = 0;
+}
diff --git a/gui/manualtest/tests/projects/custom-cfg/a.c b/gui/manualtest/tests/projects/custom-cfg/a.c
new file mode 100644
index 00000000000..58fe69254d0
--- /dev/null
+++ b/gui/manualtest/tests/projects/custom-cfg/a.c
@@ -0,0 +1,4 @@
+int main(void)
+{
+ return 0;
+}
diff --git a/gui/manualtest/tests/projects/custom-cfg/aa.cfg b/gui/manualtest/tests/projects/custom-cfg/aa.cfg
new file mode 100644
index 00000000000..bb21afac352
--- /dev/null
+++ b/gui/manualtest/tests/projects/custom-cfg/aa.cfg
@@ -0,0 +1,402 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+ 0:255
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ 0:
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/custom-cfg/p.cppcheck b/gui/manualtest/tests/projects/custom-cfg/p.cppcheck
new file mode 100644
index 00000000000..e8db10582ad
--- /dev/null
+++ b/gui/manualtest/tests/projects/custom-cfg/p.cppcheck
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/embedded/board.h b/gui/manualtest/tests/projects/embedded/board.h
new file mode 100644
index 00000000000..8a5bddc04a0
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/board.h
@@ -0,0 +1,10 @@
+#ifndef BOARD_H
+#define BOARD_H
+
+#define GPIOA_ODR (*(volatile unsigned int *)0x40020014U)
+#define UART_DR (*(volatile unsigned int *)0x40011004U)
+
+void gpio_set(unsigned int pin, unsigned int mask);
+void uart_putc(char c);
+
+#endif
diff --git a/gui/manualtest/tests/projects/embedded/embedded.cppcheck b/gui/manualtest/tests/projects/embedded/embedded.cppcheck
new file mode 100644
index 00000000000..679b87f6d90
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/embedded.cppcheck
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ misra
+
+
diff --git a/gui/manualtest/tests/projects/embedded/gpio.c b/gui/manualtest/tests/projects/embedded/gpio.c
new file mode 100644
index 00000000000..73968d86135
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/gpio.c
@@ -0,0 +1,6 @@
+#include "board.h"
+
+void gpio_set(unsigned int pin, unsigned int mask)
+{
+ GPIOA_ODR |= mask;
+}
diff --git a/gui/manualtest/tests/projects/embedded/main.c b/gui/manualtest/tests/projects/embedded/main.c
new file mode 100644
index 00000000000..6daafe949b4
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/main.c
@@ -0,0 +1,10 @@
+#include "board.h"
+
+int main(void)
+{
+ gpio_set(5U, 0x20U);
+ uart_putc('A');
+ for (;;)
+ {
+ }
+}
diff --git a/gui/manualtest/tests/projects/embedded/rules with spaces.txt b/gui/manualtest/tests/projects/embedded/rules with spaces.txt
new file mode 100644
index 00000000000..a707cb8673d
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/rules with spaces.txt
@@ -0,0 +1,7 @@
+Appendix A Summary of guidelines
+
+Rule 2.7
+TEST RULE TEXT: unused parameters are not allowed
+Category Advisory
+Analysis Decidable, Single Translation Unit
+Applies to C90, C99
diff --git a/gui/manualtest/tests/projects/embedded/rules.txt b/gui/manualtest/tests/projects/embedded/rules.txt
new file mode 100644
index 00000000000..a707cb8673d
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/rules.txt
@@ -0,0 +1,7 @@
+Appendix A Summary of guidelines
+
+Rule 2.7
+TEST RULE TEXT: unused parameters are not allowed
+Category Advisory
+Analysis Decidable, Single Translation Unit
+Applies to C90, C99
diff --git a/gui/manualtest/tests/projects/embedded/uart.c b/gui/manualtest/tests/projects/embedded/uart.c
new file mode 100644
index 00000000000..3720b2fde22
--- /dev/null
+++ b/gui/manualtest/tests/projects/embedded/uart.c
@@ -0,0 +1,6 @@
+#include "board.h"
+
+void uart_putc(char c)
+{
+ UART_DR = (unsigned int)c;
+}
diff --git a/gui/manualtest/tests/projects/recheck/a.c b/gui/manualtest/tests/projects/recheck/a.c
new file mode 100644
index 00000000000..a4f00f729cb
--- /dev/null
+++ b/gui/manualtest/tests/projects/recheck/a.c
@@ -0,0 +1,5 @@
+int main(void)
+{
+ int *p = 0;
+ return *p;
+}
diff --git a/gui/manualtest/tests/projects/recheck/a_v2.c b/gui/manualtest/tests/projects/recheck/a_v2.c
new file mode 100644
index 00000000000..229abfebb92
--- /dev/null
+++ b/gui/manualtest/tests/projects/recheck/a_v2.c
@@ -0,0 +1,5 @@
+int main(void)
+{
+ int x;
+ return x;
+}
diff --git a/gui/manualtest/tests/projects/recheck/b.c b/gui/manualtest/tests/projects/recheck/b.c
new file mode 100644
index 00000000000..20be2edc145
--- /dev/null
+++ b/gui/manualtest/tests/projects/recheck/b.c
@@ -0,0 +1,5 @@
+void f(void)
+{
+ char a[4];
+ a[10] = 0;
+}
diff --git a/gui/manualtest/tests/projects/recheck/p.cppcheck b/gui/manualtest/tests/projects/recheck/p.cppcheck
new file mode 100644
index 00000000000..760eb8837f8
--- /dev/null
+++ b/gui/manualtest/tests/projects/recheck/p.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/relative-paths/compile_commands.json b/gui/manualtest/tests/projects/relative-paths/compile_commands.json
new file mode 100644
index 00000000000..130de6bca98
--- /dev/null
+++ b/gui/manualtest/tests/projects/relative-paths/compile_commands.json
@@ -0,0 +1,4 @@
+[
+ {"directory": ".", "command": "gcc -c src/a.c", "file": "src/a.c"},
+ {"directory": ".", "command": "gcc -c skip/b.c", "file": "skip/b.c"}
+]
diff --git a/gui/manualtest/tests/projects/relative-paths/inc/util.h b/gui/manualtest/tests/projects/relative-paths/inc/util.h
new file mode 100644
index 00000000000..874a45c458c
--- /dev/null
+++ b/gui/manualtest/tests/projects/relative-paths/inc/util.h
@@ -0,0 +1 @@
+int helper(int x);
diff --git a/gui/manualtest/tests/projects/relative-paths/skip/b.c b/gui/manualtest/tests/projects/relative-paths/skip/b.c
new file mode 100644
index 00000000000..5bb061143c0
--- /dev/null
+++ b/gui/manualtest/tests/projects/relative-paths/skip/b.c
@@ -0,0 +1,4 @@
+int unused(void)
+{
+ return 0;
+}
diff --git a/gui/manualtest/tests/projects/relative-paths/src/a.c b/gui/manualtest/tests/projects/relative-paths/src/a.c
new file mode 100644
index 00000000000..db8654dea9f
--- /dev/null
+++ b/gui/manualtest/tests/projects/relative-paths/src/a.c
@@ -0,0 +1,6 @@
+#include "util.h"
+
+int main(void)
+{
+ return helper(1);
+}
diff --git a/gui/manualtest/tests/projects/switch-project/one/a.c b/gui/manualtest/tests/projects/switch-project/one/a.c
new file mode 100644
index 00000000000..a4f00f729cb
--- /dev/null
+++ b/gui/manualtest/tests/projects/switch-project/one/a.c
@@ -0,0 +1,5 @@
+int main(void)
+{
+ int *p = 0;
+ return *p;
+}
diff --git a/gui/manualtest/tests/projects/switch-project/one/p1.cppcheck b/gui/manualtest/tests/projects/switch-project/one/p1.cppcheck
new file mode 100644
index 00000000000..5eb66587401
--- /dev/null
+++ b/gui/manualtest/tests/projects/switch-project/one/p1.cppcheck
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/switch-project/two/b.c b/gui/manualtest/tests/projects/switch-project/two/b.c
new file mode 100644
index 00000000000..20be2edc145
--- /dev/null
+++ b/gui/manualtest/tests/projects/switch-project/two/b.c
@@ -0,0 +1,5 @@
+void f(void)
+{
+ char a[4];
+ a[10] = 0;
+}
diff --git a/gui/manualtest/tests/projects/switch-project/two/p2.cppcheck b/gui/manualtest/tests/projects/switch-project/two/p2.cppcheck
new file mode 100644
index 00000000000..a79d287d385
--- /dev/null
+++ b/gui/manualtest/tests/projects/switch-project/two/p2.cppcheck
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/binary.c b/gui/manualtest/tests/projects/syntax-error/binary.c
new file mode 100644
index 00000000000..c91312c3af0
Binary files /dev/null and b/gui/manualtest/tests/projects/syntax-error/binary.c differ
diff --git a/gui/manualtest/tests/projects/syntax-error/binary.cppcheck b/gui/manualtest/tests/projects/syntax-error/binary.cppcheck
new file mode 100644
index 00000000000..7804897af2d
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/binary.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/braces.c b/gui/manualtest/tests/projects/syntax-error/braces.c
new file mode 100644
index 00000000000..66d81f572c3
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/braces.c
@@ -0,0 +1,3 @@
+int main( {
+ return 0
+}
diff --git a/gui/manualtest/tests/projects/syntax-error/braces.cppcheck b/gui/manualtest/tests/projects/syntax-error/braces.cppcheck
new file mode 100644
index 00000000000..94d206fd67c
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/braces.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/empty.c b/gui/manualtest/tests/projects/syntax-error/empty.c
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/gui/manualtest/tests/projects/syntax-error/empty.cppcheck b/gui/manualtest/tests/projects/syntax-error/empty.cppcheck
new file mode 100644
index 00000000000..eb36efbe086
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/empty.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/latin1.c b/gui/manualtest/tests/projects/syntax-error/latin1.c
new file mode 100644
index 00000000000..7e730fd3a83
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/latin1.c
@@ -0,0 +1,6 @@
+/* färg och smör */
+int färg(void)
+{
+ const char *s = "grön";
+ return s[0];
+}
diff --git a/gui/manualtest/tests/projects/syntax-error/latin1.cppcheck b/gui/manualtest/tests/projects/syntax-error/latin1.cppcheck
new file mode 100644
index 00000000000..06ccf58dfd5
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/latin1.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/long_line.c b/gui/manualtest/tests/projects/syntax-error/long_line.c
new file mode 100644
index 00000000000..14807c9e52b
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/long_line.c
@@ -0,0 +1 @@
+int x = a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a +
diff --git a/gui/manualtest/tests/projects/syntax-error/long_line.cppcheck b/gui/manualtest/tests/projects/syntax-error/long_line.cppcheck
new file mode 100644
index 00000000000..f13ad9aa112
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/long_line.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/syntax-error/ok.c b/gui/manualtest/tests/projects/syntax-error/ok.c
new file mode 100644
index 00000000000..8e1e58ce827
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/ok.c
@@ -0,0 +1,5 @@
+int ok(void)
+{
+ int *p = 0;
+ return *p;
+}
diff --git a/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.c b/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.c
new file mode 100644
index 00000000000..7d6ac74625a
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.c
@@ -0,0 +1,4 @@
+int f(int a)
+{
+ return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((a;
+}
diff --git a/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.cppcheck b/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.cppcheck
new file mode 100644
index 00000000000..baeddf3ef2f
--- /dev/null
+++ b/gui/manualtest/tests/projects/syntax-error/unclosed_nesting.cppcheck
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/projects/xml-results/a.c b/gui/manualtest/tests/projects/xml-results/a.c
new file mode 100644
index 00000000000..38775adee5f
--- /dev/null
+++ b/gui/manualtest/tests/projects/xml-results/a.c
@@ -0,0 +1,5 @@
+int first(void)
+{
+ int *p = 0;
+ return *p;
+}
diff --git a/gui/manualtest/tests/projects/xml-results/results.xml b/gui/manualtest/tests/projects/xml-results/results.xml
new file mode 100644
index 00000000000..c6f4b14c0c1
--- /dev/null
+++ b/gui/manualtest/tests/projects/xml-results/results.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/gui/manualtest/tests/result_list.feature b/gui/manualtest/tests/result_list.feature
new file mode 100644
index 00000000000..7b20fadc61f
--- /dev/null
+++ b/gui/manualtest/tests/result_list.feature
@@ -0,0 +1,90 @@
+Feature: Results list
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:t852206f1
+ Scenario: Severity buttons toggle
+ When I click each of the toolbar buttons Error, Warning, Style, Portability, Performance, Information, Cppcheck and Clang once
+ Then every button can be clicked and changes between checked and unchecked
+ When I click each of them once more
+ Then all buttons are in the state they had at the start
+
+ @id:td7decf1a @project:basic-project
+ Scenario: Filter the results
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I type "arrayIndex" in the Filter field
+ Then only the arrayIndexOutOfBounds error in b.c is shown
+ When I clear the Filter field
+ Then all errors are shown again
+
+ @id:t2af9d498 @project:basic-project
+ Scenario: Expand all
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I open View > Collapse All
+ And I open View > Expand All
+ Then all files are expanded and show their errors
+ And the errors in a.c, util.h and b.c are visible
+
+ @id:tec898362 @project:basic-project
+ Scenario: Collapse all
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I open View > Expand All
+ And I open View > Collapse All
+ Then all files are collapsed
+ And the file entries are shown and their errors are not
+
+ @id:t4c027507 @project:basic-project
+ Scenario: Context menu of a file entry
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I right click on the file a.c in the results (not on one of its errors)
+ Then the context menu of the file entry has the entries Recheck and Open containing folder
+ But it has none of the entries Hide, Hide all with ID or Suppress selected id, they only apply to a single error
+
+ @id:taacc3841 @project:basic-project
+ Scenario: Hide one error
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I expand a.c in the results
+ And I right click on the first nullPointer error (line 6) and click Hide
+ Then that error is gone
+ And the other errors are still shown, including the second nullPointer error in a.c (line 12)
+
+ @id:t2937b690 @project:basic-project
+ Scenario: Hide all errors with the same ID
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I expand a.c in the results
+ And I right click on a nullPointer error and click Hide all with ID
+ Then all nullPointer errors are gone, in a.c and in util.h
+ And the arrayIndexOutOfBounds error in b.c is still shown
+
+ @id:t313463d3 @project:basic-project
+ Scenario: Show hidden errors again
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I expand a.c in the results
+ And I right click on the first nullPointer error and click Hide
+ And I open View > Show Hidden
+ Then the error that was hidden is shown again
+
+ @id:t90bf32e9 @project:basic-project
+ Scenario: Suppress selected ID
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I expand a.c in the results
+ And I right click on a nullPointer error and click Suppress selected id
+ And I open File > Edit Project File and go to Warning options
+ Then no nullPointer error is shown in the results
+ And the id nullPointer is listed under Suppressions in the project file dialog
+
+ @id:t389a5f3a @project:basic-project
+ Scenario: Clear results
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I click Clear results
+ Then the results are cleared: the result tree is empty
diff --git a/gui/manualtest/tests/result_list.scripts.json b/gui/manualtest/tests/result_list.scripts.json
new file mode 100644
index 00000000000..6495e860cb2
--- /dev/null
+++ b/gui/manualtest/tests/result_list.scripts.json
@@ -0,0 +1,1081 @@
+{
+ "t852206f1": {
+ "script": [
+ {
+ "do": "click",
+ "name": "Show errors",
+ "role": "check box",
+ "label": "Click Error once"
+ },
+ {
+ "do": "click",
+ "name": "Show warnings",
+ "role": "check box",
+ "label": "Click Warning once"
+ },
+ {
+ "do": "click",
+ "name": "Show style warnings",
+ "role": "check box",
+ "label": "Click Style once"
+ },
+ {
+ "do": "click",
+ "name": "Show portability",
+ "role": "check box",
+ "label": "Click Portability once"
+ },
+ {
+ "do": "click",
+ "name": "Show performance warnings",
+ "role": "check box",
+ "label": "Click Performance once"
+ },
+ {
+ "do": "click",
+ "name": "Show information",
+ "role": "check box",
+ "label": "Click Information once"
+ },
+ {
+ "do": "click",
+ "name": "Cppcheck",
+ "role": "check box",
+ "label": "Click Cppcheck once"
+ },
+ {
+ "do": "click",
+ "name": "Clang",
+ "role": "check box",
+ "label": "Click Clang once"
+ },
+ {
+ "expect": "state",
+ "name": "Show errors",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Error changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Show warnings",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Warning changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Show style warnings",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Style changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Show portability",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Portability changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Show performance warnings",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Performance changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Show information",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Information changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Cppcheck",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Cppcheck changed to unchecked after first click"
+ },
+ {
+ "expect": "state",
+ "name": "Clang",
+ "role": "check box",
+ "not_state": "checked",
+ "label": "Clang changed to unchecked after first click"
+ },
+ {
+ "do": "click",
+ "name": "Show errors",
+ "role": "check box",
+ "label": "Click Error a second time"
+ },
+ {
+ "do": "click",
+ "name": "Show warnings",
+ "role": "check box",
+ "label": "Click Warning a second time"
+ },
+ {
+ "do": "click",
+ "name": "Show style warnings",
+ "role": "check box",
+ "label": "Click Style a second time"
+ },
+ {
+ "do": "click",
+ "name": "Show portability",
+ "role": "check box",
+ "label": "Click Portability a second time"
+ },
+ {
+ "do": "click",
+ "name": "Show performance warnings",
+ "role": "check box",
+ "label": "Click Performance a second time"
+ },
+ {
+ "do": "click",
+ "name": "Show information",
+ "role": "check box",
+ "label": "Click Information a second time"
+ },
+ {
+ "do": "click",
+ "name": "Cppcheck",
+ "role": "check box",
+ "label": "Click Cppcheck a second time"
+ },
+ {
+ "do": "click",
+ "name": "Clang",
+ "role": "check box",
+ "label": "Click Clang a second time"
+ },
+ {
+ "expect": "state",
+ "name": "Show errors",
+ "role": "check box",
+ "state": "checked",
+ "label": "Error is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Show warnings",
+ "role": "check box",
+ "state": "checked",
+ "label": "Warning is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Show style warnings",
+ "role": "check box",
+ "state": "checked",
+ "label": "Style is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Show portability",
+ "role": "check box",
+ "state": "checked",
+ "label": "Portability is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Show performance warnings",
+ "role": "check box",
+ "state": "checked",
+ "label": "Performance is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Show information",
+ "role": "check box",
+ "state": "checked",
+ "label": "Information is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Cppcheck",
+ "role": "check box",
+ "state": "checked",
+ "label": "Cppcheck is back to its start state (checked)"
+ },
+ {
+ "expect": "state",
+ "name": "Clang",
+ "role": "check box",
+ "state": "checked",
+ "label": "Clang is back to its start state (checked)"
+ }
+ ],
+ "script_for": "e5baac140df9"
+ },
+ "td7decf1a": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "exact": true,
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis is done and results are shown"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "All files are listed before filtering"
+ },
+ {
+ "expect": "visible",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "util.h is listed before filtering"
+ },
+ {
+ "do": "focus",
+ "path": "0.4.1",
+ "role": "text",
+ "label": "Focus the Filter field"
+ },
+ {
+ "do": "type",
+ "value": "arrayIndex",
+ "label": "Type arrayIndex in the Filter field"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "label": "Only the b.c error remains shown"
+ },
+ {
+ "expect": "gone",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "a.c error is filtered out"
+ },
+ {
+ "expect": "gone",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "util.h error is filtered out"
+ },
+ {
+ "do": "settext",
+ "path": "0.4.1",
+ "role": "text",
+ "value": "",
+ "label": "Clear the Filter field"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "a.c error is shown again"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "label": "b.c error is shown again"
+ },
+ {
+ "expect": "visible",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "util.h error is shown again"
+ }
+ ],
+ "script_for": "f86b9b8205a7"
+ },
+ "t2af9d498": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "do": "wait",
+ "name": "Checking src/b.c",
+ "role": "list item",
+ "timeout": 30,
+ "label": "Wait until the analysis has run"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Results are shown"
+ },
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu"
+ },
+ {
+ "do": "click",
+ "name": "Collapse all",
+ "role": "menu item",
+ "label": "View > Collapse All"
+ },
+ {
+ "do": "click",
+ "name": "Expand all",
+ "role": "menu item",
+ "label": "View > Expand All (menu is still open)"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: r",
+ "role": "table cell",
+ "label": "The error in util.h is visible"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "The error in a.c is visible"
+ },
+ {
+ "expect": "state",
+ "name": "include/util.h",
+ "role": "table cell",
+ "state": "expanded",
+ "nth": 0,
+ "label": "util.h is expanded"
+ },
+ {
+ "expect": "state",
+ "name": "src/a.c",
+ "role": "table cell",
+ "state": "expanded",
+ "nth": 0,
+ "label": "a.c is expanded"
+ }
+ ],
+ "script_for": "3d1da0542440"
+ },
+ "tec898362": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "File dialog appears"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 60,
+ "label": "Analysis is done and the file entries are shown"
+ },
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu"
+ },
+ {
+ "do": "click",
+ "name": "Expand all",
+ "role": "menu item",
+ "label": "View > Expand All"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "Errors are visible after expanding"
+ },
+ {
+ "do": "hover",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu again"
+ },
+ {
+ "do": "click",
+ "name": "Collapse all",
+ "role": "menu item",
+ "label": "View > Collapse All"
+ },
+ {
+ "expect": "visible",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "File entry util.h is shown"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "File entry a.c is shown"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "label": "File entry b.c is shown"
+ },
+ {
+ "expect": "gone",
+ "name": "nullPointer",
+ "label": "Errors are not shown"
+ },
+ {
+ "expect": "gone",
+ "name": "constVariablePointer",
+ "label": "Style errors are not shown"
+ }
+ ],
+ "script_for": "405a34af72d2"
+ },
+ "t4c027507": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File from the File menu"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "Wait for the file dialog"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the path of basic.cppcheck"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "do": "wait",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 60,
+ "label": "Wait until the analysis is done and results are shown"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "gone": true,
+ "label": "File dialog is closed"
+ },
+ {
+ "do": "rightclick",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Right click on the file a.c in the results"
+ },
+ {
+ "expect": "ocr",
+ "contains": "Recheck",
+ "label": "Context menu has Recheck"
+ },
+ {
+ "expect": "ocr",
+ "contains": "Open containing folder",
+ "label": "Context menu has Open containing folder"
+ },
+ {
+ "expect": "ocr",
+ "not_contains": "Hide",
+ "label": "Context menu has no Hide entries"
+ },
+ {
+ "expect": "ocr",
+ "not_contains": "Suppress selected id",
+ "label": "Context menu has no Suppress selected id"
+ }
+ ],
+ "script_for": "237d77caee36"
+ },
+ "taacc3841": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis is done and results show a.c"
+ },
+ {
+ "do": "focus",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Focus the a.c row"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand a.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "First nullPointer error (line 6) is shown"
+ },
+ {
+ "do": "rightclick",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "Right click on the first nullPointer error"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Down",
+ "Down",
+ "Return"
+ ],
+ "label": "Click Hide (Hide 1 result(s)) in the context menu"
+ },
+ {
+ "expect": "gone",
+ "name": "Null pointer dereference: p",
+ "label": "The hidden error is gone"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: q",
+ "role": "table cell",
+ "label": "Second nullPointer error (line 12) is still shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Variable 'p' can be declared as pointer to const",
+ "role": "table cell",
+ "label": "Other errors are still shown (line 5)"
+ },
+ {
+ "expect": "visible",
+ "name": "Variable 'q' can be declared as pointer to const",
+ "role": "table cell",
+ "label": "Other errors are still shown (line 11)"
+ }
+ ],
+ "script_for": "13f9f46bd6a3"
+ },
+ "t2937b690": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Wait until the analysis is done and results are shown"
+ },
+ {
+ "do": "doubleclick",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Select a.c in the results"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand a.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "nullPointer errors are listed before hiding"
+ },
+ {
+ "do": "rightclick",
+ "name": "nullPointer",
+ "role": "table cell",
+ "nth": 0,
+ "label": "Right click on a nullPointer error"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Down",
+ "Down",
+ "Down"
+ ],
+ "label": "Move to Hide all with id in the context menu"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Click Hide all with ID"
+ },
+ {
+ "expect": "gone",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "All nullPointer errors are gone from a.c and util.h"
+ },
+ {
+ "expect": "visible",
+ "name": "constVariablePointer",
+ "role": "table cell",
+ "label": "Other errors in a.c are still shown"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "End",
+ "Right"
+ ],
+ "label": "Expand b.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "arrayIndexOutOfBounds",
+ "role": "table cell",
+ "label": "The arrayIndexOutOfBounds error in b.c is still shown"
+ }
+ ],
+ "script_for": "0c3a81139be4"
+ },
+ "t313463d3": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "nth": 1,
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis is done and results are shown"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Let the analysis finish completely"
+ },
+ {
+ "do": "click",
+ "name": "src/a.c",
+ "role": "table cell",
+ "mouse": true,
+ "label": "Select a.c in the results"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand a.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "The first nullPointer error is listed"
+ },
+ {
+ "do": "rightclick",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "Right click on the first nullPointer error"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Down",
+ "Down",
+ "Return"
+ ],
+ "label": "Click Hide in the context menu"
+ },
+ {
+ "expect": "gone",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "The error is hidden"
+ },
+ {
+ "do": "click",
+ "name": "View",
+ "role": "menu item",
+ "label": "Open the View menu"
+ },
+ {
+ "do": "click",
+ "name": "Show hidden",
+ "role": "menu item",
+ "label": "Click Show Hidden"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "timeout": 5,
+ "observed_fail": true,
+ "label": "The hidden error is shown again"
+ }
+ ],
+ "script_for": "1675a0aff0c3"
+ },
+ "t90bf32e9": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File",
+ "role": "menu item",
+ "label": "Click Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter project file path"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "exact": true,
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "src/a.c",
+ "role": "table cell",
+ "timeout": 60,
+ "label": "Wait until the analysis is done"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Let the analysis finish"
+ },
+ {
+ "do": "focus",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Select a.c in the results"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand a.c in the results"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "nullPointer error is shown under a.c"
+ },
+ {
+ "do": "rightclick",
+ "name": "nullPointer",
+ "role": "table cell",
+ "nth": 0,
+ "label": "Right click on a nullPointer error"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Down",
+ "Down",
+ "Down",
+ "Down",
+ "Down",
+ "Return"
+ ],
+ "label": "Click Suppress selected id"
+ },
+ {
+ "expect": "gone",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "No nullPointer error is shown in the results"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open File menu"
+ },
+ {
+ "do": "click",
+ "name": "Edit Project File",
+ "role": "menu item",
+ "label": "Click Edit Project File"
+ },
+ {
+ "do": "click",
+ "name": "Warning options",
+ "role": "page tab",
+ "label": "Go to Warning options"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "list item",
+ "label": "The id nullPointer is listed under Suppressions"
+ }
+ ],
+ "script_for": "fb50a62ef505"
+ },
+ "t389a5f3a": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "Choose Open Project File"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file path"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "exact": true,
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 30,
+ "label": "Analysis results are shown"
+ },
+ {
+ "do": "sleep",
+ "seconds": 3,
+ "label": "Wait until the analysis is done"
+ },
+ {
+ "expect": "state",
+ "name": "Clear results",
+ "role": "button",
+ "state": "enabled",
+ "label": "Analysis finished, Clear results is available"
+ },
+ {
+ "do": "click",
+ "name": "Clear results",
+ "role": "button",
+ "label": "Click Clear results"
+ },
+ {
+ "expect": "gone",
+ "name": "src/a.c",
+ "role": "table cell",
+ "label": "Result tree has no src/a.c entry"
+ },
+ {
+ "expect": "gone",
+ "name": "src/b.c",
+ "role": "table cell",
+ "label": "Result tree has no src/b.c entry"
+ },
+ {
+ "expect": "gone",
+ "name": "include/util.h",
+ "role": "table cell",
+ "label": "Result tree has no include/util.h entry"
+ }
+ ],
+ "script_for": "96a64734a43b"
+ }
+}
diff --git a/gui/manualtest/tests/xml_results.feature b/gui/manualtest/tests/xml_results.feature
new file mode 100644
index 00000000000..2a7b48c5285
--- /dev/null
+++ b/gui/manualtest/tests/xml_results.feature
@@ -0,0 +1,45 @@
+Feature: XML results
+
+ Background:
+ Given the application is started with no project loaded
+
+ @id:tc1fc409e @project:basic-project
+ Scenario Outline: Save results to a file
+ Given the project file {project:basic-project}/basic.cppcheck is opened from the File menu
+ And the analysis is done
+ When I open File > Save results to file
+ And I choose the file type for files
+ And I save the results as results. in the work directory {work}
+ Then the file results. is saved in the format
+ And it has the nullPointer error in a.c ("Null pointer dereference")
+ And it has the arrayIndexOutOfBounds error in b.c ("out of bounds")
+
+ Examples:
+ | format |
+ | xml |
+ | txt |
+ | csv |
+
+ @id:tcdb051d4 @project:xml-results
+ Scenario: Open XML results
+ When I open File > Open XML
+ And I select {project:xml-results}/results.xml
+ Then the XML results are opened
+ And the nullPointer error in a.c is shown
+
+ @id:t8d9a2076 @ticket:13984 @project:xml-results
+ Scenario: No Recheck in XML results
+ Given the XML results {project:xml-results}/results.xml are opened with File > Open XML
+ When I right click on the file a.c in the results
+ Then the context menu of the file has no Recheck entry and no Open containing folder entry, XML results can not be analysed again
+ And the application does not crash
+
+ @id:t053b7c9c @project:xml-results @project:basic-project
+ Scenario: Reanalyze enabled after XML results
+ Given the XML results {project:xml-results}/results.xml are opened with File > Open XML
+ Then the button Reanalyze all files is disabled
+ And if I rightclick on a.c in the results I get a popup
+ And there is a option "Recheck 1 file(s)" in the popup that is disabled
+ When I open the project file {project:basic-project}/basic.cppcheck
+ And I wait until the analysis is done
+ Then the button Reanalyze all files is enabled and can be clicked
diff --git a/gui/manualtest/tests/xml_results.scripts.json b/gui/manualtest/tests/xml_results.scripts.json
new file mode 100644
index 00000000000..95fa6cebb97
--- /dev/null
+++ b/gui/manualtest/tests/xml_results.scripts.json
@@ -0,0 +1,466 @@
+{
+ "tc1fc409e": {
+ "script": [
+ {
+ "label": "Start the application with no project loaded",
+ "expect": "visible",
+ "name": "Cppcheck",
+ "role": "frame"
+ },
+ {
+ "label": "Open the File menu",
+ "do": "click",
+ "name": "File",
+ "role": "menu item"
+ },
+ {
+ "label": "Choose File > Open Project File",
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item"
+ },
+ {
+ "label": "Wait for the project file dialog",
+ "do": "wait",
+ "name": "Select Project File",
+ "role": "dialog"
+ },
+ {
+ "label": "Enter the project file basic.cppcheck",
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck"
+ },
+ {
+ "label": "Open the project file",
+ "do": "key",
+ "keys": [
+ "Return"
+ ]
+ },
+ {
+ "label": "The project is loaded",
+ "expect": "visible",
+ "name": "Project: basic.cppcheck",
+ "role": "frame",
+ "timeout": 10
+ },
+ {
+ "label": "Analysis has produced results for b.c",
+ "expect": "visible",
+ "name": "src/b.c",
+ "role": "table cell",
+ "timeout": 30
+ },
+ {
+ "label": "Wait until the analysis is done",
+ "expect": "state",
+ "name": "Stop analysis",
+ "role": "button",
+ "not_state": "enabled",
+ "timeout": 30
+ },
+ {
+ "label": "Open the File menu",
+ "do": "click",
+ "name": "File",
+ "role": "menu item"
+ },
+ {
+ "label": "Open File > Save results to file",
+ "do": "click",
+ "name": "Save results to file...",
+ "role": "menu item"
+ },
+ {
+ "label": "Wait for the save dialog",
+ "do": "wait",
+ "name": "Save the report file",
+ "role": "dialog"
+ },
+ {
+ "label": "Open the Files of type list",
+ "do": "click",
+ "name": "XML files",
+ "role": "combo box",
+ "mouse": true
+ },
+ {
+ "label": "Choose the file type for {format} files",
+ "do": "click",
+ "name": "(*.{format})",
+ "role": "list item",
+ "mouse": true
+ },
+ {
+ "label": "Enter results.{format} in the work directory as file name",
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{work}/results.{format}"
+ },
+ {
+ "label": "Save the results",
+ "do": "click",
+ "name": "Save",
+ "role": "button",
+ "exact": true
+ },
+ {
+ "label": "The save dialog is closed",
+ "expect": "gone",
+ "name": "Save the report file",
+ "role": "dialog"
+ },
+ {
+ "label": "results.{format} is saved and names a.c",
+ "expect": "file",
+ "path": "{work}/results.{format}",
+ "contains": "src/a.c"
+ },
+ {
+ "label": "results.{format} has the nullPointer error in a.c (Null pointer dereference)",
+ "expect": "file",
+ "path": "{work}/results.{format}",
+ "contains": "Null pointer dereference"
+ },
+ {
+ "label": "results.{format} has the arrayIndexOutOfBounds error in b.c (out of bounds)",
+ "expect": "file",
+ "path": "{work}/results.{format}",
+ "regex": "src/b.c[\\s\\S]*out of bounds|out of bounds[\\s\\S]*src/b.c"
+ }
+ ],
+ "script_for": "dad1485bb95a"
+ },
+ "tcdb051d4": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open XML",
+ "role": "menu item",
+ "label": "Choose File > Open XML"
+ },
+ {
+ "expect": "visible",
+ "name": "Open the report file",
+ "role": "dialog",
+ "label": "The file dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:xml-results}/results.xml",
+ "label": "Select results.xml of the xml-results project"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Confirm the file dialog"
+ },
+ {
+ "do": "wait",
+ "name": "Open the report file",
+ "gone": true,
+ "label": "The file dialog closes"
+ },
+ {
+ "expect": "visible",
+ "name": "a.c",
+ "role": "table cell",
+ "label": "The file a.c is listed in the results"
+ },
+ {
+ "do": "focus",
+ "name": "a.c",
+ "role": "table cell",
+ "label": "Give keyboard focus to the a.c entry"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Right"
+ ],
+ "label": "Expand the a.c entry to see its error"
+ },
+ {
+ "expect": "visible",
+ "name": "nullPointer",
+ "role": "table cell",
+ "label": "The nullPointer error is shown"
+ },
+ {
+ "expect": "visible",
+ "name": "Null pointer dereference: p",
+ "role": "table cell",
+ "label": "The nullPointer summary is shown"
+ },
+ {
+ "expect": "visible",
+ "name": "error",
+ "role": "table cell",
+ "label": "The severity of the error is shown"
+ }
+ ],
+ "script_for": "35589d1420ec"
+ },
+ "t8d9a2076": {
+ "script": [
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open XML",
+ "role": "menu item",
+ "label": "Choose File > Open XML"
+ },
+ {
+ "do": "wait",
+ "name": "Open the report file",
+ "timeout": 10,
+ "label": "Wait for the open dialog"
+ },
+ {
+ "do": "settext",
+ "name": "File name",
+ "role": "text",
+ "value": "{project:xml-results}/results.xml",
+ "label": "Enter the path of results.xml"
+ },
+ {
+ "do": "click",
+ "name": "Open",
+ "role": "button",
+ "nth": 1,
+ "label": "Confirm the file dialog"
+ },
+ {
+ "expect": "visible",
+ "name": "a.c",
+ "role": "table cell",
+ "timeout": 10,
+ "label": "The results show the file a.c"
+ },
+ {
+ "do": "rightclick",
+ "name": "a.c",
+ "role": "table cell",
+ "label": "Right click on the file a.c in the results"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Menu"
+ ],
+ "label": "Make sure the context menu is open"
+ },
+ {
+ "expect": "gone",
+ "name": "Recheck",
+ "label": "The context menu has no Recheck entry"
+ },
+ {
+ "expect": "gone",
+ "name": "Open containing folder",
+ "label": "The context menu has no Open containing folder entry"
+ },
+ {
+ "expect": "alive",
+ "label": "The application does not crash"
+ }
+ ],
+ "script_for": "a23ab5dd15e4"
+ },
+ "t053b7c9c": {
+ "script": [
+ {
+ "expect": "visible",
+ "name": "Cppcheck",
+ "role": "frame",
+ "label": "Given the application is started with no project loaded"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "Given the XML results are opened with File > Open XML: open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open XML...",
+ "role": "menu item",
+ "label": "Given the XML results are opened with File > Open XML: choose Open XML"
+ },
+ {
+ "expect": "visible",
+ "name": "Open the report file",
+ "role": "dialog",
+ "label": "The Open XML file dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:xml-results}/results.xml",
+ "label": "Enter the XML results file name"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the XML results"
+ },
+ {
+ "do": "wait",
+ "name": "Open the report file",
+ "gone": true,
+ "timeout": 10,
+ "label": "Wait until the file dialog is closed"
+ },
+ {
+ "expect": "visible",
+ "name": "results.xml",
+ "role": "frame",
+ "timeout": 10,
+ "label": "The XML results are loaded"
+ },
+ {
+ "expect": "visible",
+ "name": "a.c",
+ "role": "table cell",
+ "timeout": 10,
+ "label": "a.c is shown in the results"
+ },
+ {
+ "expect": "state",
+ "name": "Reanalyze all files",
+ "role": "button",
+ "not_state": "enabled",
+ "label": "Then the button Reanalyze all files is disabled"
+ },
+ {
+ "do": "rightclick",
+ "name": "a.c",
+ "role": "table cell",
+ "label": "And if I rightclick on a.c in the results"
+ },
+ {
+ "do": "sleep",
+ "seconds": 1
+ },
+ {
+ "expect": "ocr",
+ "contains": "Copy",
+ "label": "I get a popup"
+ },
+ {
+ "expect": "ocr",
+ "not_contains": "Recheck",
+ "label": "There is an option Recheck 1 file(s) in the popup that is disabled (greyed out, not read by OCR)"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Escape"
+ ],
+ "label": "Close the popup"
+ },
+ {
+ "do": "click",
+ "name": "File",
+ "role": "menu item",
+ "label": "When I open the project file: open the File menu"
+ },
+ {
+ "do": "click",
+ "name": "Open Project File...",
+ "role": "menu item",
+ "label": "When I open the project file: choose Open Project File"
+ },
+ {
+ "expect": "visible",
+ "name": "Select Project File",
+ "role": "dialog",
+ "label": "The project file dialog is shown"
+ },
+ {
+ "do": "settext",
+ "name": "File name:",
+ "role": "text",
+ "value": "{project:basic-project}/basic.cppcheck",
+ "label": "Enter the project file name"
+ },
+ {
+ "do": "key",
+ "keys": [
+ "Return"
+ ],
+ "label": "Open the project file"
+ },
+ {
+ "do": "wait",
+ "name": "Select Project File",
+ "gone": true,
+ "timeout": 10,
+ "label": "Wait until the file dialog is closed"
+ },
+ {
+ "expect": "visible",
+ "name": "Project: basic.cppcheck",
+ "role": "frame",
+ "timeout": 10,
+ "label": "The project is loaded"
+ },
+ {
+ "expect": "state",
+ "name": "Stop analysis",
+ "role": "button",
+ "not_state": "enabled",
+ "timeout": 30,
+ "label": "And I wait until the analysis is done"
+ },
+ {
+ "expect": "state",
+ "name": "Reanalyze all files",
+ "role": "button",
+ "state": "enabled",
+ "timeout": 30,
+ "label": "Then the button Reanalyze all files is enabled"
+ },
+ {
+ "do": "click",
+ "name": "Reanalyze all files",
+ "role": "button",
+ "label": "And can be clicked"
+ },
+ {
+ "expect": "alive",
+ "label": "The application still runs after clicking Reanalyze all files"
+ },
+ {
+ "expect": "state",
+ "name": "Reanalyze all files",
+ "role": "button",
+ "state": "enabled",
+ "timeout": 30,
+ "label": "Reanalyze all files is enabled again after the reanalysis"
+ }
+ ],
+ "script_for": "299ae476b3db"
+ }
+}
diff --git a/gui/manualtest/web.py b/gui/manualtest/web.py
new file mode 100644
index 00000000000..3deab02fb49
--- /dev/null
+++ b/gui/manualtest/web.py
@@ -0,0 +1,463 @@
+#!/usr/bin/env python3
+"""Web interface for editing, generating and running the Gherkin tests.
+
+ web.py [--opensource APP] [--premium APP] [--src DIR] [--dir tests] [--out results] [--port 8080]
+
+Serves on 127.0.0.1 only. Reads and writes DIR/*.feature, with the scripts in DIR/*.scripts.json,
+and starts gen.py and run.py with the application that is tested, one binary per edition. SRC is the git checkout that both are built
+from, for the git sha in the reports. --app is the same as --opensource. Without any APP the
+tests can be edited but not generated or run.
+"""
+
+import argparse
+import json
+import mimetypes
+import os
+import re
+import signal
+import subprocess
+import sys
+import shutil
+import threading
+import time
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from urllib.parse import parse_qs, quote, unquote, urlparse
+
+import testfile
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+NAME_RE = re.compile(r'^[\w.-]+\.feature$')
+ID_RE = re.compile(r'^\w+$')
+
+
+class Job:
+ """gen.py or run.py in the background, one at a time. The output is shown in the web page.
+ A run of several editions is one run.py after the other, each with its own report."""
+
+ def __init__(self, kind, file, test_id, cmds, out_dir):
+ self.kind, self.file, self.test_id, self.out_dir = kind, file, test_id, out_dir
+ self.editions = [e for e, _ in cmds]
+ self.lines, self.reports, self.rc = [], [], None
+ self.started = time.time()
+ self.progress = {'total': 0, 'index': 0, 'test': '', 'step': 0, 'steps': 0, 'label': '', 'base': None,
+ 'edition': None, 'results': []}
+ self.proc, self.stopped, self.lock = None, False, threading.Lock()
+ threading.Thread(target=self.work, args=(cmds,), daemon=True).start()
+
+ def work(self, cmds):
+ rc = 0
+ for edition, cmd in cmds:
+ with self.lock:
+ if self.stopped:
+ break
+ self.edition = edition
+ self.proc = subprocess.Popen(cmd, cwd=HERE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
+ env=dict(os.environ, PYTHONUNBUFFERED='1'), start_new_session=True)
+ if len(cmds) > 1:
+ self.lines.append('=== %s ===' % edition)
+ self.read()
+ rc = max(rc, self.proc.wait())
+ self.rc = rc
+
+ def stop(self):
+ with self.lock:
+ self.stopped = True
+ if self.proc and self.proc.poll() is None:
+ self.proc.send_signal(signal.SIGINT) # run.py writes its report, gen.py cleans up
+
+ def read(self):
+ for line in self.proc.stdout:
+ line = line.rstrip('\n')
+ if line.startswith('@{') and self.event(line[1:]):
+ continue
+ self.lines.append(line)
+ m = re.match(r'report: (.+)$', line)
+ if m:
+ self.reports.append({'edition': self.edition,
+ 'url': '/results/' + os.path.relpath(m.group(1), self.out_dir)})
+
+ def event(self, text):
+ """A progress line from run.py --events. False when it is not one."""
+ try:
+ e = json.loads(text)
+ kind, p = e['kind'], self.progress
+ except (ValueError, KeyError, TypeError):
+ return False
+ if kind == 'start':
+ p.update(total=e['total'], index=0, test='', step=0, steps=0, label='', edition=e.get('edition'),
+ base='/results/' + os.path.relpath(e['out'], self.out_dir))
+ elif kind == 'test':
+ p.update(index=e['index'], test=e['name'], step=0, steps=e['steps'], label='')
+ elif kind == 'step':
+ p.update(step=e['step'], steps=e['steps'], label=e['label'])
+ elif kind == 'result':
+ p['results'].append(dict({k: e[k] for k in ('name', 'status', 'reason', 'seconds', 'crash', 'slug')},
+ shots=e.get('shots', []), edition=p['edition'], base=p['base']))
+ return True
+
+ def info(self):
+ return {'kind': self.kind, 'file': self.file, 'id': self.test_id, 'running': self.rc is None,
+ 'rc': self.rc, 'output': '\n'.join(self.lines), 'reports': self.reports, 'editions': self.editions,
+ 'progress': self.progress, 'started': self.started, 'now': time.time()}
+
+
+class Handler(BaseHTTPRequestHandler):
+ directory = None
+ out_dir = None
+ apps = {} # edition -> path of the binary
+ src = None
+ job = None
+ lock = threading.Lock()
+
+ def log_message(self, fmt, *args):
+ pass
+
+ def reply(self, code, data, ctype='application/json'):
+ if ctype == 'application/json':
+ data = json.dumps(data, ensure_ascii=False)
+ if isinstance(data, str):
+ data = data.encode()
+ self.send_response(code)
+ self.send_header('Content-Type', ctype + ('; charset=utf-8' if ctype.startswith(('text/', 'application/json')) else ''))
+ self.send_header('Content-Length', str(len(data)))
+ self.send_header('Cache-Control', 'no-store')
+ self.end_headers()
+ self.wfile.write(data)
+
+ def allowed(self):
+ # only localhost names as Host (DNS rebinding) and JSON for writes (cross-site forms)
+ host = self.headers.get('Host', '').rsplit(':', 1)[0]
+ if host not in ('127.0.0.1', 'localhost'):
+ self.reply(403, {'error': 'bad host'})
+ return False
+ if self.command in ('PUT', 'POST') and self.headers.get('Content-Type', '').split(';')[0] != 'application/json':
+ self.reply(415, {'error': 'application/json required'})
+ return False
+ return True
+
+ def path_of(self, query):
+ name = parse_qs(query).get('name', [''])[0]
+ if not NAME_RE.match(name):
+ self.reply(400, {'error': 'bad file name (letters, digits, _ - . and .feature)'})
+ return None
+ return os.path.join(self.directory, name)
+
+ def project_path(self, query):
+ name = parse_qs(query).get('name', [''])[0]
+ if not testfile.PROJECT_RE.match(name):
+ self.reply(400, {'error': 'bad project name (letters, digits, _ and -)'})
+ return None
+ return os.path.join(self.directory, testfile.PROJECTS, name)
+
+ def used_by(self, project):
+ used = []
+ for f in sorted(os.listdir(self.directory)):
+ if NAME_RE.match(f):
+ try:
+ tests = testfile.load(os.path.join(self.directory, f)).get('tests', [])
+ except (ValueError, OSError):
+ continue
+ used += ['%s: %s' % (f, t.get('name')) for t in tests if project in t.get('projects', [])]
+ return used
+
+ def reports(self):
+ """The earlier runs in the out directory, newest first."""
+ found = []
+ for d in os.listdir(self.out_dir) if os.path.isdir(self.out_dir) else []:
+ if not os.path.isfile(os.path.join(self.out_dir, d, 'report.html')):
+ continue
+ try:
+ with open(os.path.join(self.out_dir, d, 'results.json'), encoding='utf-8') as f:
+ run = json.load(f)
+ counts = {}
+ for r in run.get('results', []):
+ counts[r['status']] = counts.get(r['status'], 0) + 1
+ except (ValueError, OSError, KeyError, TypeError):
+ run, counts = {}, {}
+ found.append({'id': d, 'url': '/results/%s/report.html' % quote(d), 'started': run.get('started', ''),
+ 'edition': run.get('edition'), 'branch': run.get('branch'), 'sha': (run.get('sha') or '')[:8],
+ 'dirty': run.get('dirty'), 'aborted': run.get('aborted'), 'counts': counts,
+ 'files': [f.get('title') or f.get('file') for f in run.get('files', [])]})
+ return sorted(found, key=lambda r: r['id'], reverse=True)
+
+ def annotate(self, doc):
+ """Add _stale (the script is outdated) and _errors (what is wrong with the test or its script)
+ to every test, for the web page. Not saved, see stamp_scripts."""
+ tests = doc.get('tests') if isinstance(doc, dict) else None
+ background = doc.get('background') if isinstance(doc, dict) and isinstance(doc.get('background'), list) else []
+ for t in tests if isinstance(tests, list) else []:
+ if not isinstance(t, dict):
+ continue
+ t['_stale'] = testfile.is_stale(t, self.directory, background)
+ t['_errors'] = testfile.validate({'tests': [t]}, self.directory)
+
+ def file_status(self):
+ """{file: {invalid: n, stale: n}}: tests with errors and tests whose script must be generated again.
+ invalid is 1 for a file that is not valid Gherkin."""
+ out = {}
+ for name in os.listdir(self.directory):
+ if not NAME_RE.match(name):
+ continue
+ try:
+ doc = testfile.ensure_ids(testfile.load(os.path.join(self.directory, name)))
+ self.annotate(doc)
+ tests = [t for t in doc['tests'] if isinstance(t, dict)]
+ bad = sum(1 for t in tests if t['_errors']) + (len(tests) != len(doc['tests'])) + bool(
+ testfile.validate(dict(doc, tests=[]), self.directory))
+ out[name] = {'invalid': bad, 'stale': sum(1 for t in tests if t['_stale'])}
+ except (ValueError, OSError, KeyError, TypeError, AttributeError):
+ out[name] = {'invalid': 1, 'stale': 0}
+ return out
+
+ def body(self):
+ return json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))) or b'null')
+
+ def do_GET(self):
+ if not self.allowed():
+ return
+ u = urlparse(self.path)
+ if u.path == '/':
+ with open(os.path.join(HERE, 'web', 'index.html'), 'rb') as f:
+ self.reply(200, f.read(), 'text/html')
+ elif u.path == '/api/files':
+ self.reply(200, sorted(f for f in os.listdir(self.directory) if NAME_RE.match(f)))
+ elif u.path == '/api/file-status':
+ self.reply(200, self.file_status())
+ elif u.path == '/api/script-doc':
+ self.reply(200, testfile.SCRIPT_DOC, 'text/plain')
+ elif u.path == '/api/config':
+ self.reply(200, {'apps': self.apps, 'editions': list(testfile.EDITIONS), 'can_run': bool(self.apps)})
+ elif u.path == '/api/job':
+ self.reply(200, self.job.info() if self.job else None)
+ elif u.path == '/api/projects':
+ base = os.path.join(self.directory, testfile.PROJECTS)
+ names = sorted(n for n in os.listdir(base) if os.path.isdir(os.path.join(base, n))) if os.path.isdir(base) else []
+ self.reply(200, [{'name': n, 'used_by': self.used_by(n)} for n in names])
+ elif u.path == '/api/project':
+ path = self.project_path(u.query)
+ if path:
+ if not os.path.isdir(path):
+ return self.reply(404, {'error': 'no such project'})
+ files, binary = {}, []
+ for rel, data in sorted(testfile.project_files(self.directory, os.path.basename(path)).items()):
+ try:
+ files[rel] = data.decode('utf-8')
+ except UnicodeDecodeError:
+ binary.append(rel)
+ self.reply(200, {'files': files, 'binary': binary, 'used_by': self.used_by(os.path.basename(path))})
+ elif u.path == '/api/reports':
+ self.reply(200, self.reports())
+ elif u.path.startswith('/results/'):
+ self.static_file(self.out_dir, unquote(u.path[len('/results/'):]))
+ elif u.path.startswith('/docs/'):
+ self.static_file(os.path.join(HERE, 'docs'), unquote(u.path[len('/docs/'):]))
+ elif u.path == '/api/file':
+ path = self.path_of(u.query)
+ if path:
+ try:
+ doc = testfile.ensure_ids(testfile.load(path))
+ except FileNotFoundError:
+ return self.reply(404, {'error': 'no such file'})
+ except ValueError as e:
+ return self.reply(400, {'error': '%s is not valid Gherkin (fix it in a text editor): %s' % (
+ os.path.basename(path), e)})
+ self.annotate(doc)
+ self.reply(200, doc)
+ else:
+ self.reply(404, {'error': 'not found'})
+
+ def static_file(self, base, rel):
+ """A file below BASE: the results of the runs or the handbook."""
+ base = os.path.realpath(base)
+ path = os.path.realpath(os.path.join(base, rel))
+ if not path.startswith(base + os.sep) or not os.path.isfile(path):
+ return self.reply(404, {'error': 'not found'})
+ ctype = 'text/plain' if path.endswith('.log') else mimetypes.guess_type(path)[0] or 'text/plain'
+ with open(path, 'rb') as f:
+ self.reply(200, f.read(), ctype)
+
+ def do_PUT(self):
+ if not self.allowed():
+ return
+ u = urlparse(self.path)
+ if u.path == '/api/project':
+ return self.put_project(u.query)
+ path = self.path_of(u.query) if u.path == '/api/file' else None
+ if not path:
+ return
+ try:
+ doc = self.body()
+ except ValueError:
+ return self.reply(400, {'errors': ['not valid JSON']})
+ errors = testfile.validate(doc, self.directory)
+ if errors:
+ return self.reply(400, {'errors': errors})
+ try:
+ old = testfile.load(path)
+ except (FileNotFoundError, ValueError):
+ old = None
+ testfile.save(path, testfile.stamp_scripts(doc, old, self.directory))
+ self.reply(200, {'saved': os.path.basename(path)})
+
+ def put_project(self, query):
+ path = self.project_path(query)
+ if not path:
+ return
+ try:
+ files = self.body()['files']
+ assert isinstance(files, dict) and all(isinstance(v, str) for v in files.values())
+ except (ValueError, KeyError, TypeError, AssertionError):
+ return self.reply(400, {'errors': ['expected {"files": {"path": "content"}}']})
+ clean = {}
+ for rel, content in files.items():
+ norm = os.path.normpath(rel)
+ if (not rel or os.path.isabs(norm) or norm.startswith('..') or '\0' in rel or norm == '.'
+ or norm.split(os.sep)[0] == '..'):
+ return self.reply(400, {'errors': ['bad file path: %r (relative, inside the project)' % rel]})
+ clean[norm] = content
+ if sum(len(c) for c in clean.values()) > 4_000_000:
+ return self.reply(400, {'errors': ['the project is too big for this editor']})
+ os.makedirs(path, exist_ok=True)
+ # text files that are not in the request were removed in the editor, binary files stay
+ for rel, data in testfile.project_files(self.directory, os.path.basename(path)).items():
+ if rel not in clean:
+ try:
+ data.decode('utf-8')
+ except UnicodeDecodeError:
+ continue
+ os.remove(os.path.join(path, rel))
+ for rel, content in clean.items():
+ target = os.path.join(path, rel)
+ os.makedirs(os.path.dirname(target), exist_ok=True)
+ with open(target + '.tmp', 'w', encoding='utf-8', newline='') as f:
+ f.write(content)
+ os.replace(target + '.tmp', target)
+ for dirpath, dirs, fs in os.walk(path, topdown=False): # directories that became empty
+ if dirpath != path and not os.listdir(dirpath):
+ os.rmdir(dirpath)
+ self.reply(200, {'saved': os.path.basename(path), 'files': len(clean)})
+
+ def do_POST(self):
+ if not self.allowed():
+ return
+ route = urlparse(self.path).path
+ try:
+ data = self.body()
+ if route == '/api/job':
+ self.start_job(data)
+ elif route == '/api/job/stop':
+ if self.job and self.job.rc is None:
+ self.job.stop()
+ self.reply(200, {'stopping': True})
+ else:
+ self.reply(404, {'error': 'not found'})
+ except (ValueError, KeyError, TypeError):
+ self.reply(400, {'errors': ['bad request']})
+
+ def start_job(self, data):
+ action, name, test_id, edition = data['action'], data['file'], data.get('id'), data.get('edition')
+ if not self.apps:
+ return self.reply(400, {'errors': ['web.py was started without --opensource or --premium']})
+ everything = action == 'run' and name == '*' # every test file in the directory
+ if action not in ('generate', 'run') or not (everything or NAME_RE.match(name)) or (
+ test_id and not ID_RE.match(test_id)):
+ return self.reply(400, {'errors': ['bad request']})
+ if action == 'generate' and not test_id:
+ return self.reply(400, {'errors': ['generate needs a test id']})
+ if everything:
+ paths = sorted(os.path.join(self.directory, n) for n in os.listdir(self.directory) if NAME_RE.match(n))
+ if not paths or test_id:
+ return self.reply(404, {'errors': ['no test files']})
+ else:
+ paths = [os.path.join(self.directory, name)]
+ if not os.path.isfile(paths[0]):
+ return self.reply(404, {'errors': ['no such file']})
+ path = paths[0]
+ # the editions of the job: the chosen ones that web.py has a binary for and, for one test, that it is for
+ editions = [e for e in testfile.EDITIONS if e in self.apps and edition in (e, 'both')]
+ if test_id:
+ test = next((t for t in testfile.ensure_ids(testfile.load(path)).get('tests', []) if t['id'] == test_id), None)
+ if not test:
+ return self.reply(404, {'errors': ['no such test']})
+ allowed = [e for e in testfile.editions(test) if e in self.apps]
+ if action == 'generate' and not set(editions) & set(allowed):
+ editions = allowed # the script is the same for all editions, make it with one that we have
+ editions = [e for e in editions if e in allowed]
+ if not editions:
+ return self.reply(400, {'errors': ['the test is not for the edition %s, or web.py has no application for it' % (edition or '')]})
+ if action == 'generate':
+ editions = editions[:1]
+ cmds = []
+ for e in editions:
+ app = self.apps[e]
+ if action == 'generate':
+ cmd = [sys.executable, 'gen.py', app, path, '--force', '--id', test_id, '--edition', e]
+ else:
+ cmd = [sys.executable, 'run.py', app, *paths, '--out', self.out_dir, '--events', '--edition', e]
+ cmd += ['--src', self.src] if self.src else []
+ cmd += ['--id', test_id] if test_id else []
+ cmds.append((e, cmd))
+ with self.lock:
+ if self.job and self.job.rc is None:
+ return self.reply(409, {'errors': ['a job is already running']})
+ Handler.job = Job(action, name, test_id, cmds, self.out_dir)
+ self.reply(200, self.job.info())
+
+ def do_DELETE(self):
+ if not self.allowed():
+ return
+ u = urlparse(self.path)
+ if u.path == '/api/project':
+ path = self.project_path(u.query)
+ if path:
+ used = self.used_by(os.path.basename(path))
+ if used:
+ return self.reply(409, {'errors': ['the project is used by: ' + '; '.join(used)]})
+ if not os.path.isdir(path):
+ return self.reply(404, {'error': 'no such project'})
+ shutil.rmtree(path)
+ self.reply(200, {'deleted': os.path.basename(path)})
+ return
+ path = self.path_of(u.query) if u.path == '/api/file' else None
+ if path:
+ try:
+ os.remove(path)
+ if os.path.exists(testfile.scripts_path(path)):
+ os.remove(testfile.scripts_path(path))
+ self.reply(200, {'deleted': os.path.basename(path)})
+ except FileNotFoundError:
+ self.reply(404, {'error': 'no such file'})
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ for e in testfile.EDITIONS:
+ ap.add_argument('--' + e, *(['--app'] if e == 'opensource' else []), metavar='APP',
+ help='the %s edition of the application to test' % e)
+ ap.add_argument('--src', help='git checkout of the application, the same for all editions (default: the directory of APP)')
+ ap.add_argument('--dir', default=os.path.join(HERE, 'tests'))
+ ap.add_argument('--out', default=os.path.join(HERE, 'results'))
+ ap.add_argument('--port', type=int, default=8080)
+ args = ap.parse_args()
+ for e in testfile.EDITIONS:
+ app = getattr(args, e)
+ if app:
+ if not os.access(app, os.X_OK):
+ sys.exit('error: not executable: ' + app)
+ Handler.apps[e] = os.path.abspath(app)
+ Handler.src = os.path.abspath(args.src) if args.src else None
+ Handler.directory = os.path.abspath(args.dir)
+ Handler.out_dir = os.path.realpath(args.out)
+ os.makedirs(Handler.directory, exist_ok=True)
+ os.makedirs(Handler.out_dir, exist_ok=True)
+ Handler.out_dir = os.path.realpath(Handler.out_dir)
+ server = ThreadingHTTPServer(('127.0.0.1', args.port), Handler)
+ print('http://127.0.0.1:%d/ (%s)' % (args.port, Handler.directory), flush=True)
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+
+
+if __name__ == '__main__':
+ main()
diff --git a/gui/manualtest/web/index.html b/gui/manualtest/web/index.html
new file mode 100644
index 00000000000..3012a4ef925
--- /dev/null
+++ b/gui/manualtest/web/index.html
@@ -0,0 +1,677 @@
+
+
+
+
+
+GUI tester
+
+
+
+
+