├── src ├── robot_out_stream │ ├── py.typed │ ├── robot_version.py │ └── _decoder.py └── setup.py ├── tests ├── .gitignore ├── robot_out_stream_tests │ ├── __init__.py │ ├── test_robot_stream │ │ ├── another.robot │ │ ├── robot6.robot │ │ ├── robot7.robot │ │ ├── robot4.robot │ │ ├── robot11.robot │ │ ├── robot5.robot │ │ ├── robot3.robot │ │ ├── sub │ │ │ └── another_sub.robot │ │ ├── robot9.robot │ │ ├── robot8.robot │ │ ├── robot10.robot │ │ ├── robot_nolog_by_tag.robot │ │ ├── robot_nolog.robot │ │ ├── robot_nolog_arguments.robot │ │ ├── robot_nolog_try_except.robot │ │ ├── robot_nolog_args_by_name.robot │ │ ├── robot_nolog_args_by_tag.robot │ │ ├── test_robot_no_log_by_tag.yml │ │ ├── .vscode │ │ │ └── launch.json │ │ ├── test_robot_tags.yml │ │ ├── interactive_console_output.xml │ │ ├── robot1.robot │ │ ├── robot2.robot │ │ ├── test_robot_embed_img.yml │ │ ├── test_robot_assign.yml │ │ ├── test_robot_stream_unexpected_errors.yml │ │ ├── test_robot_no_log.yml │ │ ├── test_robot_no_log_try_except.yml │ │ ├── test_robot_while.yml │ │ ├── test_robot_no_log_args.yml │ │ ├── test_robot_return.yml │ │ ├── test_robot_no_log_args_by_tag.yml │ │ ├── test_robot_try_except.yml │ │ ├── test_robot_if.yml │ │ └── test_robot_no_log_args_by_name.yml │ ├── _resources │ │ └── robot1.robot │ ├── test_utilities.py │ ├── test_view_integrated.py │ ├── test_output_as_logthtml.py │ ├── test_decode_xml │ │ ├── output_5.xml │ │ ├── output_9.xml │ │ ├── output_4.xml │ │ ├── test_decode_output_5.yml │ │ ├── output_3.xml │ │ ├── test_decode_output_9.yml │ │ ├── test_decode_output_4.yml │ │ ├── output_6.xml │ │ ├── output_10.xml │ │ ├── output_2.xml │ │ ├── test_decode_output_3.yml │ │ ├── test_decode_output_2.yml │ │ ├── output_8.xml │ │ ├── output_1.xml │ │ ├── output_7.xml │ │ ├── test_decode_output_6.yml │ │ ├── test_decode_output_1.yml │ │ ├── test_decode_output_10.yml │ │ ├── test_decode_output_8.yml │ │ └── test_decode_output_7.yml │ ├── test_utilities │ │ └── test_gen_id.yml │ ├── test_decode_xml.py │ ├── test_impl_recovery.py │ └── fixtures.py ├── conftest.py └── test_requirements.txt ├── dev_requirements.txt ├── .settings ├── org.eclipse.core.resources.prefs ├── org.python.pydev.analysis.yaml └── org.python.pydev.yaml ├── output-webview ├── tests │ ├── .gitignore │ ├── _resources │ │ ├── case1.rfstream │ │ ├── case3.rfstream │ │ ├── case2.rfstream │ │ └── scenario_generator.robot │ ├── tests.ts │ ├── README.md │ ├── robot.yaml │ ├── conda.yaml │ ├── test_output_view.robot │ └── uris.py ├── src │ ├── debounce.ts │ ├── options.ts │ ├── handleLevel.ts │ ├── handleStatus.ts │ ├── runSelection.ts │ ├── summaryBuilder.ts │ ├── protocols.ts │ ├── index.html │ ├── persistTree.ts │ ├── plainDom.ts │ ├── index.ts │ ├── sample.ts │ ├── decoder.ts │ ├── vscodeComm.ts │ ├── style.css │ └── tree.ts ├── tsconfig.json ├── package.json └── webpack.config.js ├── .project ├── pyproject.toml ├── .pydevproject ├── COPYRIGHT ├── .gitattributes ├── docs ├── release.md ├── changelog.md ├── arguments.md ├── handling_sensitive_data.md └── format.md ├── .github └── workflows │ ├── release-robotframework-output-stream.yaml │ ├── linting.yml │ └── tests-robot-out-stream.yml ├── .gitignore ├── README.md └── dev.py /src/robot_out_stream/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /out_test/ 2 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = [ 2 | "robot_out_stream_tests.fixtures", 3 | ] 4 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | black==22.12.0 2 | click==8.1.3 3 | mypy==0.910 4 | types-docutils 5 | types-PyYAML 6 | fire 7 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/another.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Another keyword 3 | No Operation -------------------------------------------------------------------------------- /tests/test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | pytest 3 | pytest-regressions==1.0.6 4 | pytest-xdist 5 | pytest-timeout 6 | psutil -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot6.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check for 3 | ${a} ${b}= Evaluate [2,3] -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot7.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check for 3 | [Tags] tag nice 1 another tag 2 4 | No Operation 5 | 6 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot4.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check for 3 | FOR ${counter} IN RANGE 0 1 4 | No Operation 5 | END -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot11.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check log html 3 | Log html=True 4 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//.settings/org.python.pydev.analysis.yaml=UTF-8 3 | encoding//.settings/org.python.pydev.yaml=UTF-8 4 | -------------------------------------------------------------------------------- /.settings/org.python.pydev.analysis.yaml: -------------------------------------------------------------------------------- 1 | MYPY_USE_CONSOLE: false 2 | SEARCH_MYPY_LOCATION: SEARCH 3 | USE_MYPY: true 4 | MYPY_ADD_PROJECT_FOLDERS_TO_MYPYPATH: true 5 | MYPY_FILE_LOCATION: '' 6 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot5.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check while 3 | ${a}= Evaluate 2 4 | WHILE $a < 1 5 | ${a}= Evaluate $a-1 6 | END -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot3.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ./sub/another_sub.robot 3 | 4 | 5 | *** Test Cases *** 6 | First test 7 | Another in sub keyword 8 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/sub/another_sub.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library lib_not_there.py 3 | 4 | *** Keywords *** 5 | Another in sub keyword 6 | No Operation 7 | Log Some error message level=ERROR -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot9.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check if 3 | TRY 4 | Fail message 5 | EXCEPT message 6 | No Operation 7 | FINALLY 8 | No Operation 9 | END 10 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot8.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Check if 3 | ${var1}= Set Variable 2 4 | IF ${var1} == ${var1} 5 | No Operation 6 | ELSE IF ${var1} == ${var1} 7 | No Operation 8 | ELSE 9 | No Operation 10 | END 11 | -------------------------------------------------------------------------------- /output-webview/tests/.gitignore: -------------------------------------------------------------------------------- 1 | testrun/ 2 | output/ 3 | venv/ 4 | temp/ 5 | .rpa/ 6 | .idea/ 7 | .ipynb_checkpoints/ 8 | */.ipynb_checkpoints/* 9 | .virtual_documents/ 10 | */.ipynb_checkpoints/* 11 | .vscode 12 | .DS_Store 13 | *.pyc 14 | *.zip 15 | .~lock* 16 | */work-items-out/* 17 | interactive_console_output.xml -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot10.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library String 3 | *** Keywords *** 4 | Some Keyword 5 | ${value}= Set Variable retval 6 | ${rem}= Remove String ${value} a 7 | RETURN ${rem} ${value} 8 | 9 | 10 | *** Test Cases *** 11 | Check if 12 | Some Keyword 13 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/_resources/robot1.robot: -------------------------------------------------------------------------------- 1 | *** settings *** 2 | Library Collections 3 | 4 | *** Tasks *** 5 | Simple Task 6 | Log áéíóú 7 | ${dct}= Create Dictionary a=1 b=1 8 | Log ${dct} 9 | FOR ${counter} IN RANGE 0 10 10 | Log ${counter} 11 | 12 | END 13 | 14 | -------------------------------------------------------------------------------- /output-webview/src/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (func, wait) => { 2 | let timeout: NodeJS.Timeout; 3 | 4 | return function wrapper(...args) { 5 | const later = () => { 6 | clearTimeout(timeout); 7 | func(...args); 8 | }; 9 | 10 | clearTimeout(timeout); 11 | timeout = setTimeout(later, wait); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /output-webview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": false, 5 | "module": "es6", 6 | "target": "es6", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | robotframework-output-stream 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog_by_tag.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | Some Keyword 8 | 9 | 10 | *** Keywords *** 11 | This should not be logged 12 | [Arguments] ${arg1} ${arg2} 13 | Log To Console ${arg1} - ${arg2} 14 | 15 | Some Keyword 16 | [Tags] log:ignore-keywords 17 | This should not be logged 1 2 18 | -------------------------------------------------------------------------------- /output-webview/src/options.ts: -------------------------------------------------------------------------------- 1 | import { IOpts } from "./protocols"; 2 | import { getState } from "./vscodeComm"; 3 | 4 | let _opts: IOpts = { 5 | initialContents: undefined, 6 | runId: undefined, 7 | state: undefined, 8 | onClickReference: undefined, 9 | appendedContents: [], 10 | allRunIdsToLabel: {}, 11 | }; 12 | 13 | export function getOpts(): IOpts { 14 | if (_opts.state === undefined) { 15 | _opts.state = getState(); 16 | } 17 | return _opts; 18 | } 19 | -------------------------------------------------------------------------------- /output-webview/tests/_resources/case1.rfstream: -------------------------------------------------------------------------------- 1 | V 1 2 | T 2022-11-16T15:35:07.554 3 | I "sys.platform=win32" 4 | I "python=3.8.1 (default, Jan 8 2020, 15:55:49) [MSC v.1916 64 bit (AMD64)]" 5 | I "robot=5.0rc1" 6 | M b:"Robot1" 7 | M c:"s1" 8 | M d:"C:/Users/fabio/AppData/Local/Temp/pytest-of-fabio/pytest-1151/test_robot_out_stream0/test_robot_stream/robot1.robot" 9 | SS b|c|d|0.055 10 | M e:"Simple Task" 11 | M f:"s1-t1" 12 | ST e|f|16|0.056 13 | M m:"PASS" 14 | M h:"" 15 | ET m|h|0.095 16 | ES Y|0.096 -------------------------------------------------------------------------------- /output-webview/tests/_resources/case3.rfstream: -------------------------------------------------------------------------------- 1 | V 1 2 | T 2022-11-16T15:35:07.554 3 | I "sys.platform=win32" 4 | I "python=3.8.1 (default, Jan 8 2020, 15:55:49) [MSC v.1916 64 bit (AMD64)]" 5 | I "robot=5.0rc1" 6 | M b:"Robot1" 7 | M c:"s1" 8 | M d:"C:/Users/fabio/AppData/Local/Temp/pytest-of-fabio/pytest-1151/test_robot_out_stream0/test_robot_stream/robot1.robot" 9 | M e:"Simple Task" 10 | M f:"s1-t1" 11 | M m:"PASS" 12 | M h:"" 13 | RS b|c|d|0.055 14 | RT e|f|16|0.056 15 | ET m|h|0.095 16 | ES Y|0.096 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | mypy_path = "robotframework-output-stream/src:robotframework-output-stream/tests" 3 | 4 | [[tool.mypy.overrides]] 5 | module = "setuptools.*" 6 | ignore_missing_imports = true 7 | 8 | [[tool.mypy.overrides]] 9 | module = "robot.*" 10 | ignore_missing_imports = true 11 | 12 | [[tool.mypy.overrides]] 13 | module = "pytest_timeout.*" 14 | ignore_missing_imports = true 15 | 16 | [[tool.mypy.overrides]] 17 | module = "pytest_timeout.*" 18 | ignore_missing_imports = true 19 | 20 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | Some Keyword 8 | 9 | 10 | *** Keywords *** 11 | This should not be logged 12 | [Arguments] ${arg1} ${arg2} 13 | Log Still log this level=WARN 14 | Log Dont log this 15 | 16 | Some Keyword 17 | Stop Logging keywords 18 | This should not be logged 1 2 19 | log.Start logging keywords 20 | -------------------------------------------------------------------------------- /output-webview/tests/_resources/case2.rfstream: -------------------------------------------------------------------------------- 1 | V 1 2 | T 2022-11-16T15:35:07.554 3 | I "sys.platform=win32" 4 | I "python=3.8.1 (default, Jan 8 2020, 15:55:49) [MSC v.1916 64 bit (AMD64)]" 5 | I "robot=5.0rc1" 6 | M b:"Robot1" 7 | M c:"s1" 8 | M d:"C:/Users/fabio/AppData/Local/Temp/pytest-of-fabio/pytest-1151/test_robot_out_stream0/test_robot_stream/robot1.robot" 9 | SS b|c|d|0.055 10 | M e:"Simple Task" 11 | M f:"s1-t1" 12 | ST e|f|16|0.056 13 | M m:"PASS" 14 | M h:"" 15 | RS b|c|d|0.055 16 | RT e|f|16|0.056 17 | ET m|h|0.095 18 | ES Y|0.096 -------------------------------------------------------------------------------- /output-webview/tests/tests.ts: -------------------------------------------------------------------------------- 1 | import { setContents } from "../src/index"; 2 | import { ISetContentsRequest } from "../src/vscodeComm"; 3 | 4 | export function setupScenario(scenario) { 5 | console.log("Setup Scenario"); 6 | 7 | const msg: ISetContentsRequest = { 8 | type: "request", 9 | command: "setContents", 10 | initialContents: scenario, 11 | runId: undefined, 12 | allRunIdsToLabel: undefined, 13 | }; 14 | setContents(msg); 15 | } 16 | 17 | window["setupScenario"] = setupScenario; 18 | -------------------------------------------------------------------------------- /src/robot_out_stream/robot_version.py: -------------------------------------------------------------------------------- 1 | def _get_robot_naked_version(): 2 | import robot # noqa 3 | 4 | return str(robot.get_version(True)) 5 | 6 | 7 | _found_major_version = None 8 | 9 | 10 | def get_robot_major_version() -> int: 11 | global _found_major_version 12 | if _found_major_version is not None: 13 | return _found_major_version 14 | 15 | robot_version = _get_robot_naked_version() 16 | major_version = int(robot_version.split(".")[0]) 17 | _found_major_version = major_version 18 | return major_version 19 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog_arguments.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | Some Keyword 8 | 9 | 10 | *** Keywords *** 11 | Keyword to check 12 | [Arguments] ${arg1} ${arg2} 13 | Log To Console ${arg1} - ${arg2} 14 | [Return] ${arg1} - ${arg2} 15 | 16 | Some Keyword 17 | Stop Logging variables 18 | Keyword to check This should not be logged And this neither 19 | log.Start logging variables 20 | -------------------------------------------------------------------------------- /output-webview/tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing structure 2 | 3 | Tests are run with Robot Framework. 4 | 5 | The basic infrastructure is that tests are first built to the `dist-test` folder 6 | and then run with Robot Framework. 7 | 8 | Tests are compiled with `yarn build-test`. 9 | 10 | -- the `webpack.config.js` contains the configuration needed to compile both 11 | sources along with the test files. 12 | 13 | After tests are built, robotframework is used to open the browser in the 14 | generated index and then makes the running using Playwright as needed. 15 | 16 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog_try_except.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | Some Keyword 8 | 9 | 10 | *** Keywords *** 11 | This should not be logged 12 | [Arguments] ${arg1} ${arg2} 13 | Log Still log this level=WARN 14 | Log Dont log this 15 | 16 | Some Keyword 17 | Stop Logging keywords 18 | TRY 19 | This should not be logged 1 2 20 | EXCEPT 21 | log.Start logging keywords 22 | END 23 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default 5 | 6 | python interpreter 7 | 8 | 9 | /${PROJECT_DIR_NAME}/src 10 | /${PROJECT_DIR_NAME}/tests 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog_args_by_name.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | # Variables named password is not logged by default 8 | ${password}= Set Variable pass123 9 | Some Keyword ${password} 10 | 11 | # We can mark something to be hidden from any output 12 | log.Hide From Output pass456 13 | Some Keyword pass456 14 | 15 | 16 | *** Keywords *** 17 | Some Keyword 18 | [Arguments] ${password} 19 | Log ${password} 20 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | def test_gen_id(data_regression): 2 | from robot_out_stream._impl import _gen_id 3 | 4 | iter_in = _gen_id() 5 | generated = [] 6 | for _ in range(200): 7 | generated.append(next(iter_in)) 8 | 9 | data_regression.check(generated) 10 | 11 | 12 | def test_convert(): 13 | from robot_out_stream import _convert_to_bytes 14 | 15 | assert _convert_to_bytes("100") == 100 16 | assert _convert_to_bytes("100kb") == 100000 17 | assert _convert_to_bytes("1mb") == 1e6 18 | assert _convert_to_bytes("0.1mb") == 1e5 19 | -------------------------------------------------------------------------------- /output-webview/tests/_resources/scenario_generator.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library RPA.Desktop 3 | 4 | 5 | *** Test Cases *** 6 | My Test 7 | [Documentation] Note that this test can be changed as needed. The idea 8 | ... is that it can be changed as needed to provide .rfstream 9 | ... files as needed. 10 | FOR ${counter} IN RANGE 1 1000 11 | ${show_err}= Evaluate $counter % 50 == 0 12 | IF $show_err Log ${counter} level=ERROR 13 | END 14 | 15 | Screenshot test 16 | RPA.Desktop.Take Screenshot output/test_screenshot.png embed=True 17 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Apache 2.0 License 2 | 3 | Copyright 2022 Robocorp Technologies, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot_nolog_args_by_tag.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library robot_out_stream WITH NAME log 3 | 4 | 5 | *** Test Cases *** 6 | Check no log 7 | Some Keyword 8 | This should be logged 3 4 9 | 10 | 11 | *** Keywords *** 12 | These args should not be logged 13 | [Arguments] ${arg1} ${arg2} 14 | Log To Console ${arg1} - ${arg2} 15 | 16 | This should be logged 17 | [Arguments] ${arg1} ${arg2} 18 | Log To Console ${arg1} - ${arg2} 19 | 20 | Some Keyword 21 | [Tags] log:ignore-variables 22 | These args should not be logged 1 2 23 | -------------------------------------------------------------------------------- /output-webview/tests/robot.yaml: -------------------------------------------------------------------------------- 1 | # For more details on the format and content: 2 | # https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml 3 | 4 | tasks: 5 | Run Tests: 6 | shell: python -m robot --report NONE --listener RobotStackTracer --outputdir output test_output_view.robot 7 | 8 | condaConfigFile: conda.yaml 9 | 10 | environmentConfigs: 11 | - environment_windows_amd64_freeze.yaml 12 | - environment_linux_amd64_freeze.yaml 13 | - environment_darwin_amd64_freeze.yaml 14 | - conda.yaml 15 | 16 | artifactsDir: output 17 | 18 | PATH: 19 | - . 20 | PYTHONPATH: 21 | - . 22 | ignoreFiles: 23 | - .gitignore 24 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log_by_tag.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog By Tag 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - message: '' 19 | message_type: ET 20 | status: PASS 21 | time_delta_in_seconds: 0 22 | - message_type: ES 23 | status: PASS 24 | time_delta_in_seconds: 0 25 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "robotframework-lsp", 9 | "name": "Robot Framework: Launch .robot file", 10 | "request": "launch", 11 | "cwd": "^\"\\${workspaceFolder}\"", 12 | "target": "${file}", 13 | "terminal": "integrated", 14 | "env": {"PYTHONPATH": "${workspaceFolder}/../../../src"}, 15 | "args": ["--listener", "robot_out_stream.RFStream"] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_view_integrated.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import pytest 4 | 5 | 6 | def run_in_rcc(rcc_loc: str, cwd: str): 7 | import subprocess 8 | 9 | env = os.environ.copy() 10 | env.pop("PYTHONPATH", "") 11 | env.pop("PYTHONHOME", "") 12 | env.pop("VIRTUAL_ENV", "") 13 | env["PYTHONIOENCODING"] = "utf-8" 14 | env["PYTHONUNBUFFERED"] = "1" 15 | subprocess.check_call([rcc_loc] + "task run".split(), cwd=cwd, env=env) 16 | 17 | 18 | def test_output_view_integrated(rcc_loc: str, path_for_tests_robot: Path): 19 | matrix_name = os.environ.get("GITHUB_ACTIONS_MATRIX_NAME") 20 | if matrix_name: 21 | if "outviewintegrationtests" not in matrix_name: 22 | pytest.skip(f"Disabled for matrix name: {matrix_name}") 23 | 24 | robot_yaml = path_for_tests_robot / "robot.yaml" 25 | assert robot_yaml.exists() 26 | run_in_rcc(rcc_loc, str(path_for_tests_robot)) 27 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_tags.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot7 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check for 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - message_type: TG 19 | tag: another tag 2 20 | - message_type: TG 21 | tag: tag nice 1 22 | - doc: 23 | keyword_type: KEYWORD 24 | libname: BuiltIn 25 | lineno: 4 26 | message_type: SK 27 | name: No Operation 28 | source: 29 | time_delta_in_seconds: 0 30 | - message_type: EK 31 | status: PASS 32 | time_delta_in_seconds: 0 33 | - message: '' 34 | message_type: ET 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - message_type: ES 38 | status: PASS 39 | time_delta_in_seconds: 0 40 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/interactive_console_output.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | All Tests 15 | 16 | 17 | 18 | 19 | Robot Interactive Console 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_output_as_logthtml.py: -------------------------------------------------------------------------------- 1 | def test_output_as_loghtml(tmpdir, resources_dir): 2 | import os 3 | 4 | log_output = tmpdir / "log.html" 5 | outdir_to_listener = tmpdir / "out" 6 | max_file_size = "3MB" 7 | max_files = 2 8 | 9 | robot_file = os.path.join(resources_dir, "robot1.robot") 10 | 11 | replaced_outdir_to_listener = str(outdir_to_listener).replace(":", "") 12 | replaced_log_output = str(log_output).replace(":", "") 13 | 14 | import robot 15 | 16 | default_log_output = str(tmpdir / "log_original.html") 17 | default_xml_output = str(tmpdir / "out.xml") 18 | 19 | robot.run_cli( 20 | [ 21 | "-l", 22 | default_log_output, 23 | "-r", 24 | "None", 25 | "-o", 26 | default_xml_output, 27 | "--listener", 28 | f"robot_out_stream.RFStream:--dir={replaced_outdir_to_listener}:--max-file-size={max_file_size}:--max-files={max_files}:--log={replaced_log_output}", 29 | str(robot_file), 30 | ], 31 | exit=False, 32 | ) 33 | 34 | assert os.path.exists(log_output) 35 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot1.robot: -------------------------------------------------------------------------------- 1 | *** settings *** 2 | Library Collections 3 | Resource ./another.robot 4 | Resource ./sub/another_sub.robot 5 | 6 | *** Keywords *** 7 | First keyword 8 | No operation 9 | 10 | Log Some warning message level=WARN 11 | Another keyword 12 | Another in sub keyword 13 | 14 | 15 | *** Tasks *** 16 | Simple Task 17 | First Keyword 18 | Log Some 19 | ${dct}= Create Dictionary a=1 b=1 20 | Log ${dct} 21 | 22 | Check 1 23 | First Keyword 24 | 25 | FOR ${counter} IN RANGE 0 3 26 | IF ${counter} == 2 27 | Fail Failed execution for some reason... 28 | END 29 | Log ${counter} 30 | END 31 | 32 | Check 2 33 | ${counter}= Set Variable 3 34 | WHILE ${counter} <= 2 35 | ${counter}= Evaluate $counter-1 36 | Log Current counter: ${counter} level=WARN 37 | END 38 | 39 | Check 3 40 | TRY 41 | No Operation 42 | EXCEPT message 43 | No Operation 44 | FINALLY 45 | No Operation 46 | END -------------------------------------------------------------------------------- /output-webview/src/handleLevel.ts: -------------------------------------------------------------------------------- 1 | import { IContentAdded } from "./protocols"; 2 | 3 | function translateLevel(level: string): string { 4 | // ERROR = E 5 | // FAIL = F 6 | // INFO = I 7 | // WARN = W 8 | switch (level) { 9 | case "E": 10 | return "ERROR"; 11 | case "F": 12 | return "FAIL"; 13 | case "W": 14 | return "WARN"; 15 | case "I": 16 | return "INFO"; 17 | 18 | default: 19 | return level; 20 | } 21 | } 22 | 23 | export function getIntLevelFromLevelStr(level: string): number { 24 | switch (level) { 25 | case "E": 26 | case "F": 27 | return 2; 28 | case "W": 29 | return 1; 30 | default: 31 | return 0; 32 | } 33 | } 34 | 35 | export function addLevel(current: IContentAdded, level: string) { 36 | const span = document.createElement("span"); 37 | span.textContent = `LOG ${translateLevel(level)}`; 38 | span.classList.add("label"); 39 | span.classList.add(level.replace(" ", "_")); 40 | current.summaryDiv.insertBefore(span, current.summaryDiv.firstChild); 41 | } 42 | -------------------------------------------------------------------------------- /output-webview/tests/conda.yaml: -------------------------------------------------------------------------------- 1 | # For more details on the format and content: 2 | # https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml 3 | # Tip: Adding a link to the release notes of the packages helps maintenance and security. 4 | 5 | channels: 6 | - conda-forge 7 | 8 | dependencies: 9 | # Define conda-forge packages here -> https://anaconda.org/search 10 | # When available, prefer the conda-forge packages over pip as installations are more efficient. 11 | - python=3.9.13 # https://pyreadiness.org/3.9/ 12 | - nodejs=16.14.2 # https://github.com/nodejs/node/blob/main/CHANGELOG.md 13 | - pip=22.1.2 # https://pip.pypa.io/en/stable/news/ 14 | - pip: 15 | # Define pip packages here -> https://pypi.org/ 16 | - robotframework-browser==13.6.0 # https://github.com/MarketSquare/robotframework-browser/releases 17 | - rpaframework==17.4.0 # https://rpaframework.org/releasenotes.html 18 | - robotframework-stacktrace==0.4.1 19 | - robotframework-output-stream==0.0.1 20 | 21 | rccPostInstall: 22 | - rfbrowser init # Initialize Playwright 23 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/robot2.robot: -------------------------------------------------------------------------------- 1 | *** settings *** 2 | Library Collections 3 | Resource ./another.robot 4 | Resource ./sub/another_sub.robot 5 | 6 | *** Keywords *** 7 | First keyword 8 | No operation 9 | 10 | Log Some warning message level=WARN 11 | Another keyword 12 | Another in sub keyword 13 | 14 | 15 | *** Tasks *** 16 | Simple Task 17 | First Keyword 18 | Log Some 19 | ${dct}= Create Dictionary a=1 b=1 20 | Log ${dct} 21 | 22 | Check 1 23 | First Keyword 24 | 25 | FOR ${counter} IN RANGE 0 1000 26 | IF ${counter} == 998 27 | Fail Failed execution for some reason... 28 | END 29 | Log ${counter} 30 | END 31 | 32 | Check 2 33 | ${counter}= Set Variable 3 34 | WHILE ${counter} <= 2 35 | ${counter}= Evaluate $counter-1 36 | Log Current counter: ${counter} level=WARN 37 | END 38 | 39 | Check 3 40 | TRY 41 | No Operation 42 | EXCEPT message 43 | No Operation 44 | FINALLY 45 | No Operation 46 | END -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_embed_img.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot11 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check log html 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: BuiltIn 21 | lineno: 3 22 | message_type: SK 23 | name: Log 24 | source: 25 | time_delta_in_seconds: 0 26 | - argument: 27 | message_type: KA 28 | - argument: html=True 29 | message_type: KA 30 | - level: I 31 | message: 32 | message_type: LH 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - message: '' 38 | message_type: ET 39 | status: PASS 40 | time_delta_in_seconds: 0 41 | - message_type: ES 42 | status: PASS 43 | time_delta_in_seconds: 0 44 | -------------------------------------------------------------------------------- /.settings/org.python.pydev.yaml: -------------------------------------------------------------------------------- 1 | ADD_COMMENTS_AT_INDENT: false 2 | ADD_NEW_LINE_AT_END_OF_FILE: true 3 | AUTOPEP8_PARAMETERS: '' 4 | BLACK_PARAMETERS: '--fast' 5 | BLANK_LINES_INNER: 1 6 | BLANK_LINES_TOP_LEVEL: 2 7 | DATE_FIELD_FORMAT: yyyy-MM-dd 8 | DATE_FIELD_NAME: __updated__ 9 | ENABLE_DATE_FIELD_ACTION: false 10 | FORMATTER_STYLE: BLACK 11 | FORMAT_BEFORE_SAVING: true 12 | FORMAT_ONLY_CHANGED_LINES: false 13 | MANAGE_BLANK_LINES: true 14 | MULTI_BLOCK_COMMENT_CHAR: '=' 15 | MULTI_BLOCK_COMMENT_SHOW_ONLY_CLASS_NAME: true 16 | MULTI_BLOCK_COMMENT_SHOW_ONLY_FUNCTION_NAME: true 17 | PYDEV_TEST_RUNNER: '2' 18 | PYDEV_TEST_RUNNER_DEFAULT_PARAMETERS: --capture=no -W ignore::DeprecationWarning -n 0 --tb=native -vv --force-regen 19 | PYDEV_USE_PYUNIT_VIEW: true 20 | SAVE_ACTIONS_ONLY_ON_WORKSPACE_FILES: true 21 | SINGLE_BLOCK_COMMENT_ALIGN_RIGHT: true 22 | SINGLE_BLOCK_COMMENT_CHAR: '-' 23 | SORT_IMPORTS_ON_SAVE: false 24 | SPACES_BEFORE_COMMENT: '2' 25 | SPACES_IN_START_COMMENT: '1' 26 | TRIM_EMPTY_LINES: true 27 | TRIM_MULTILINE_LITERALS: true 28 | USE_ASSIGN_WITH_PACES_INSIDER_PARENTESIS: false 29 | USE_OPERATORS_WITH_SPACE: true 30 | USE_SPACE_AFTER_COMMA: true 31 | USE_SPACE_FOR_PARENTESIS: false 32 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_assign.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot6 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check for 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: BuiltIn 21 | lineno: 3 22 | message_type: SK 23 | name: Evaluate 24 | source: 25 | time_delta_in_seconds: 0 26 | - assign: ${a} 27 | message_type: AS 28 | - assign: ${b} 29 | message_type: AS 30 | - argument: '[2,3]' 31 | message_type: KA 32 | - level: I 33 | message: ${a} = 2 34 | message_type: L 35 | time_delta_in_seconds: 0 36 | - level: I 37 | message: ${b} = 3 38 | message_type: L 39 | time_delta_in_seconds: 0 40 | - message_type: EK 41 | status: PASS 42 | time_delta_in_seconds: 0 43 | - message: '' 44 | message_type: ET 45 | status: PASS 46 | time_delta_in_seconds: 0 47 | - message_type: ES 48 | status: PASS 49 | time_delta_in_seconds: 0 50 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Does absolutely nothing. 7 | 8 | 9 | another tag 2 10 | tag nice 1 11 | 12 | 13 | 14 | 15 | 16 | 17 | All Tests 18 | 19 | 20 | another tag 2 21 | tag nice 1 22 | 23 | 24 | Robot7 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior for when core.autocrlf is not set 2 | * text=auto 3 | 4 | # See https://git-scm.com/docs/gitattributes 5 | # See https://help.github.com/articles/dealing-with-line-endings/ 6 | 7 | # Source code 8 | *.c text 9 | *.cpp text 10 | *.h text 11 | *.hpp text 12 | *.pxd text 13 | *.py text 14 | *.pyx text 15 | 16 | # Linux shell scripts 17 | *.sh eol=lf 18 | 19 | # Windows shell scripts 20 | *.bat eol=crlf 21 | *.cmd eol=crlf 22 | *.ps1 eol=crlf 23 | 24 | # Configuration files, scripts, documentation 25 | *.cfg text 26 | *.csv text 27 | *.ini text 28 | *.json text 29 | *.md text 30 | *.rst text 31 | *.txt text 32 | *.xml text 33 | *.yaml text 34 | *.yml text 35 | .gitignore text 36 | .mu_repo text 37 | .project text 38 | .pydevproject text 39 | 40 | # Documentation 41 | *.css text 42 | *.html text 43 | *.rst text 44 | 45 | # Image 46 | *.svg eol=lf 47 | 48 | # Binary files 49 | *.jpeg binary 50 | *.jpg binary 51 | *.pdf binary 52 | *.png binary -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Does absolutely nothing. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | All Tests 19 | 20 | 21 | 22 | 23 | Robot Check 24 | Robot Check.Checkmy 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${a} 7 | ${b} 8 | [2,3] 9 | Evaluates the given expression in Python and returns the result. 10 | ${a} = 2 11 | ${b} = 3 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | All Tests 21 | 22 | 23 | 24 | 25 | Robot6 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_stream_unexpected_errors.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - level: E 9 | message: 'RFStream internal error: Unexpected error... 10 | 11 | ' 12 | message_type: L 13 | time_delta_in_seconds: 0 14 | - lineno: 2 15 | message_type: ST 16 | name: Check log html 17 | suite_id: s1-t1 18 | time_delta_in_seconds: 0 19 | - doc: 20 | keyword_type: KEYWORD 21 | libname: BuiltIn 22 | lineno: 3 23 | message_type: SK 24 | name: Log 25 | source: 26 | time_delta_in_seconds: 0 27 | - argument: 28 | message_type: KA 29 | - argument: html=True 30 | message_type: KA 31 | - level: I 32 | message: 33 | message_type: LH 34 | time_delta_in_seconds: 0 35 | - message_type: EK 36 | status: PASS 37 | time_delta_in_seconds: 0 38 | - message: '' 39 | message_type: ET 40 | status: PASS 41 | time_delta_in_seconds: 0 42 | - message_type: ES 43 | status: PASS 44 | time_delta_in_seconds: 0 45 | - level: E 46 | message: 'RFStream internal error: RFStream Warning: unable to pop suite - s1 (empty 47 | queue). 48 | 49 | 50 | ' 51 | message_type: L 52 | time_delta_in_seconds: 0 53 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_5.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T11:15:05.139' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot7 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot7.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check for 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: Does absolutely nothing. 23 | keyword_type: KEYWORD 24 | libname: BuiltIn 25 | lineno: -1 26 | message_type: SK 27 | name: No Operation 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - message_type: S 31 | start_time_delta: 0.04 32 | - message_type: EK 33 | status: PASS 34 | time_delta_in_seconds: 0.041 35 | - message_type: TG 36 | tag: another tag 2 37 | - message_type: TG 38 | tag: tag nice 1 39 | - message_type: S 40 | start_time_delta: 0.039 41 | - message: '' 42 | message_type: ET 43 | status: PASS 44 | time_delta_in_seconds: 0.042 45 | - message_type: S 46 | start_time_delta: 0.012 47 | - message_type: ES 48 | status: PASS 49 | time_delta_in_seconds: -1.0 50 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${counter} 7 | 0 8 | 1 9 | 10 | 0 11 | 12 | Does absolutely nothing. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | All Tests 26 | 27 | 28 | 29 | 30 | Robot4 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /output-webview/src/handleStatus.ts: -------------------------------------------------------------------------------- 1 | import { IContentAdded, ILiNodesCreated, IOpts } from "./protocols"; 2 | 3 | export function addStatus(current: IContentAdded | ILiNodesCreated, status: string) { 4 | const span = document.createElement("span"); 5 | span.textContent = status; 6 | span.classList.add("label"); 7 | span.classList.add(status.replace(" ", "_")); 8 | current.summaryDiv.insertBefore(span, current.summaryDiv.firstChild); 9 | } 10 | 11 | export function addTime(current: IContentAdded, diff: number) { 12 | const span = document.createElement("span"); 13 | span.textContent = ` (${diff.toFixed(2)}s)`; 14 | span.classList.add("timeLabel"); 15 | current.summaryDiv.appendChild(span); 16 | } 17 | 18 | export function acceptLevel(opts: IOpts, statusLevel: number) { 19 | switch (opts.state.filterLevel) { 20 | case "FAIL": 21 | return statusLevel >= 2; 22 | case "WARN": 23 | return statusLevel >= 1; 24 | case "PASS": 25 | return statusLevel >= 0; 26 | case "NOT RUN": 27 | return true; 28 | } 29 | } 30 | 31 | export function getIntLevelFromStatus(status: string): number { 32 | switch (status) { 33 | case "FAIL": 34 | case "ERROR": 35 | return 2; 36 | case "WARN": 37 | return 1; 38 | case "NOT RUN": 39 | case "NOT_RUN": 40 | return -1; 41 | case "PASS": 42 | return 0; 43 | default: 44 | return 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: '' 21 | lineno: 7 22 | message_type: SK 23 | name: Some Keyword 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: log 29 | lineno: 17 30 | message_type: SK 31 | name: Stop Logging Keywords 32 | source: 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - level: W 38 | message: Still log this 39 | message_type: L 40 | time_delta_in_seconds: 0 41 | - doc: 42 | keyword_type: KEYWORD 43 | libname: log 44 | lineno: 19 45 | message_type: SK 46 | name: Start Logging Keywords 47 | source: 48 | time_delta_in_seconds: 0 49 | - message_type: EK 50 | status: PASS 51 | time_delta_in_seconds: 0 52 | - message_type: EK 53 | status: PASS 54 | time_delta_in_seconds: 0 55 | - message: '' 56 | message_type: ET 57 | status: PASS 58 | time_delta_in_seconds: 0 59 | - message_type: ES 60 | status: PASS 61 | time_delta_in_seconds: 0 62 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_9.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-11-02T14:49:40.612' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0.1 (Python 3.7.5 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot Check 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\local_test\robot_check 16 | time_delta_in_seconds: -1.0 17 | - message_type: SS 18 | name: Checkmy 19 | suite_id: s1-s1 20 | suite_source: x:\vscode-robot\local_test\robot_check\checkmy.robot 21 | time_delta_in_seconds: -1.0 22 | - lineno: 10 23 | message_type: ST 24 | name: Test 25 | suite_id: s1-s1-t1 26 | time_delta_in_seconds: -1.0 27 | - doc: Does absolutely nothing. 28 | keyword_type: KEYWORD 29 | libname: BuiltIn 30 | lineno: -1 31 | message_type: SK 32 | name: No Operation 33 | source: null 34 | time_delta_in_seconds: -1.0 35 | - message_type: S 36 | start_time_delta: 0.049 37 | - message_type: EK 38 | status: PASS 39 | time_delta_in_seconds: 0.049 40 | - message_type: S 41 | start_time_delta: 0.048 42 | - message: '' 43 | message_type: ET 44 | status: PASS 45 | time_delta_in_seconds: 0.05 46 | - message_type: S 47 | start_time_delta: 0.045 48 | - message_type: ES 49 | status: PASS 50 | time_delta_in_seconds: -1.0 51 | - message_type: S 52 | start_time_delta: 0.008 53 | - message_type: ES 54 | status: PASS 55 | time_delta_in_seconds: -1.0 56 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log_try_except.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog Try Except 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: '' 21 | lineno: 7 22 | message_type: SK 23 | name: Some Keyword 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: log 29 | lineno: 17 30 | message_type: SK 31 | name: Stop Logging Keywords 32 | source: 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - level: W 38 | message: Still log this 39 | message_type: L 40 | time_delta_in_seconds: 0 41 | - doc: 42 | keyword_type: KEYWORD 43 | libname: log 44 | lineno: 21 45 | message_type: SK 46 | name: Start Logging Keywords 47 | source: 48 | time_delta_in_seconds: 0 49 | - message_type: EK 50 | status: NOT RUN 51 | time_delta_in_seconds: 0 52 | - message_type: EK 53 | status: PASS 54 | time_delta_in_seconds: 0 55 | - message: '' 56 | message_type: ET 57 | status: PASS 58 | time_delta_in_seconds: 0 59 | - message_type: ES 60 | status: PASS 61 | time_delta_in_seconds: 0 62 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | 2 | Steps to do a new release 3 | --------------------------- 4 | 5 | - Open a shell at the proper place (something as `x:\robocorpws\robotframework-output-stream`) 6 | 7 | - Create release branch (`git branch -D release-robotframework-output-stream&git checkout -b release-robotframework-output-stream`) 8 | 9 | - When leaving pre-alpha: Update classifier in setup.py (currently in pre-alpha) and notes regarding being alpha in README.md. 10 | 11 | - Update version (`python -m dev set-version 0.0.6`). 12 | 13 | - Update embedded index.html (`python -m dev build-output-view`). 14 | 15 | - Update README.md to add notes on features/fixes (on `robotframework-output-stream`). 16 | 17 | - Update changelog.md to add notes on features/fixes and set release date. 18 | 19 | - Push contents, and check if tests passed in https://github.com/robocorp/robotframework-lsp/actions. 20 | - `mu acp Robot Framework Output Stream Release 0.0.6` 21 | 22 | - Rebase with master (`git checkout master&git rebase release-robotframework-output-stream`). 23 | 24 | - Create a tag (`git tag robotframework-output-stream-0.0.6`) and push it. 25 | 26 | - Send release msg. i.e.: 27 | 28 | Hi @channel, 29 | 30 | I'm happy to announce the release of `Robot Framework Output Stream 0.0.6`. 31 | 32 | *## Changes* 33 | 34 | 35 | `Robot Framework Output Stream` may be installed with: `pip install robotframework-output-stream`. 36 | Links: [PyPI](https://pypi.org/project/robotframework-output-stream/), [GitHub (sources)](https://github.com/robocorp/robotframework-lsp/tree/master/robotframework-output-stream) -------------------------------------------------------------------------------- /output-webview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "output-webview", 3 | "version": "0.0.1", 4 | "description": "A custom view to show Robot Framework output.", 5 | "private": true, 6 | "scripts": { 7 | "build-prod": "webpack --env production", 8 | "build-dev": "webpack --env development", 9 | "build-test": "webpack --env test", 10 | "watch-build-dev": "webpack --watch --env development", 11 | "watch-build-test": "webpack --watch --env test", 12 | "build": "webpack", 13 | "start": "webpack serve --hot --inline --progress --port 8886" 14 | }, 15 | "scriptsComments": { 16 | "Comment1": "To develop the library run `yarn install`/`yarn watch-build-dev` to automatically", 17 | "Comment2": "regenerate the contents when the webview contents are changed." 18 | }, 19 | "keywords": [], 20 | "author": "Fabio Zadrozny", 21 | "publisher": "robocorp", 22 | "license": "Apache 2.0", 23 | "prettier": { 24 | "tabWidth": 4, 25 | "printWidth": 120, 26 | "quoteProps": "preserve" 27 | }, 28 | "devDependencies": { 29 | "css-loader": "^5.2.6", 30 | "html-inline-script-webpack-plugin": "^3.1.0", 31 | "html-webpack-plugin": "^5.3.1", 32 | "prettier": "2.4.1", 33 | "style-loader": "^2.0.0", 34 | "ts-loader": "^9.2.3", 35 | "typescript": "^4.3.2", 36 | "webpack": "^5.38.1", 37 | "webpack-cli": "^4.7.2", 38 | "webpack-dev-server": "^3.11.2" 39 | }, 40 | "dependencies": { 41 | "date-fns": "^2.29.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/release-robotframework-output-stream.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "release-robotframework-output-stream" 5 | tags: 6 | - "robotframework-output-stream-*" 7 | name: Deploy - RobotFramework Output Stream 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | fail-fast: true 14 | 15 | steps: 16 | - name: Checkout repository and submodules 17 | uses: actions/checkout@v1 18 | with: 19 | submodules: recursive 20 | 21 | - name: Setup node 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 16.x 25 | 26 | - name: Set up Python 3.7 27 | uses: actions/setup-python@v1 28 | with: 29 | python-version: 3.7 30 | 31 | # Build Python version 32 | - name: Install deps 33 | run: pip install --upgrade pip fire twine wheel setuptools 34 | 35 | - name: Embed output view in index.py 36 | run: | 37 | python -m dev build-output-view 38 | # python -m dev check-no-git-changes -- disabled because 39 | # building in prod makes changes appear even if the entry 40 | # code is the same. 41 | 42 | - name: Build wheel 43 | working-directory: ./src 44 | run: | 45 | cp ../README.md ./README.md 46 | python setup.py sdist bdist_wheel --universal 47 | 48 | - name: Check tag version 49 | run: python -m dev check-tag-version 50 | 51 | - name: Upload to PyPI 52 | working-directory: ./src 53 | run: twine upload dist/* 54 | env: 55 | TWINE_USERNAME: __token__ 56 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 57 | -------------------------------------------------------------------------------- /output-webview/src/runSelection.ts: -------------------------------------------------------------------------------- 1 | import { createOption, selectById } from "./plainDom"; 2 | 3 | export function rebuildRunSelection(allRunIdsToLabel: object | undefined, currentRunId: string) { 4 | if (allRunIdsToLabel === undefined) { 5 | return; 6 | } 7 | const runSelection = selectById("selectedRun"); 8 | 9 | // Mark existing and remove stale. 10 | const foundRunIdToLabel: Map = new Map(); 11 | for (let child of runSelection.childNodes) { 12 | if (child instanceof HTMLOptionElement) { 13 | const o: HTMLOptionElement = child; 14 | const runId = o.value; 15 | const label = allRunIdsToLabel[runId]; 16 | if (label === undefined) { 17 | child.remove(); 18 | } else { 19 | if (o.text !== label) { 20 | o.text = label; 21 | } 22 | foundRunIdToLabel.set(runId, label); 23 | const selected = currentRunId == runId; 24 | o.selected = selected; 25 | } 26 | } 27 | } 28 | 29 | // Add new ones. 30 | for (const runId of Object.keys(allRunIdsToLabel)) { 31 | if (runId === undefined) { 32 | continue; 33 | } 34 | const selected = currentRunId == runId; 35 | if (!foundRunIdToLabel.has(runId)) { 36 | const opt = createOption(); 37 | const label = allRunIdsToLabel[runId]; 38 | opt.value = runId; 39 | runSelection.appendChild(opt); 40 | opt.selected = selected; 41 | opt.text = label; 42 | foundRunIdToLabel.set(runId, label); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_4.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:46:31.941' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot6 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot6.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check for 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: Evaluates the given expression in Python and returns the result. 23 | keyword_type: KEYWORD 24 | libname: BuiltIn 25 | lineno: -1 26 | message_type: SK 27 | name: Evaluate 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - assign: ${a} 31 | message_type: AS 32 | - assign: ${b} 33 | message_type: AS 34 | - argument: '[2,3]' 35 | message_type: KA 36 | - level: I 37 | message: ${a} = 2 38 | message_type: L 39 | time_delta_in_seconds: 0.046 40 | - level: I 41 | message: ${b} = 3 42 | message_type: L 43 | time_delta_in_seconds: 0.048 44 | - message_type: S 45 | start_time_delta: 0.046 46 | - message_type: EK 47 | status: PASS 48 | time_delta_in_seconds: 0.048 49 | - message_type: S 50 | start_time_delta: 0.044 51 | - message: '' 52 | message_type: ET 53 | status: PASS 54 | time_delta_in_seconds: 0.049 55 | - message_type: S 56 | start_time_delta: 0.014 57 | - message_type: ES 58 | status: PASS 59 | time_delta_in_seconds: -1.0 60 | -------------------------------------------------------------------------------- /output-webview/src/summaryBuilder.ts: -------------------------------------------------------------------------------- 1 | import { divById } from "./plainDom"; 2 | 3 | export class SummaryBuilder { 4 | totalTests: number = 0; 5 | totalFailures: number = 0; 6 | 7 | clear() { 8 | this.totalTests = 0; 9 | this.totalFailures = 0; 10 | this.updateSummary(); 11 | } 12 | 13 | onTestEndUpdateSummary(msg: any) { 14 | const status = msg.decoded["status"]; 15 | this.totalTests += 1; 16 | if (status == "FAIL" || status == "ERROR") { 17 | this.totalFailures += 1; 18 | } 19 | this.updateSummary(); 20 | } 21 | 22 | updateSummary() { 23 | const totalTestsStr = ("" + this.totalTests).padStart(4); 24 | const totalFailuresStr = ("" + this.totalFailures).padStart(4); 25 | const summary = divById("summary"); 26 | summary.textContent = `Total: ${totalTestsStr} Failures: ${totalFailuresStr}`; 27 | 28 | if (this.totalFailures == 0 && this.totalTests == 0) { 29 | const resultBar: HTMLDivElement = divById("summary"); 30 | resultBar.classList.add("NOT_RUN"); 31 | resultBar.classList.remove("PASS"); 32 | resultBar.classList.remove("FAIL"); 33 | } else if (this.totalFailures == 1) { 34 | const resultBar: HTMLDivElement = divById("summary"); 35 | resultBar.classList.remove("NOT_RUN"); 36 | resultBar.classList.remove("PASS"); 37 | resultBar.classList.add("FAIL"); 38 | } else if (this.totalFailures == 0 && this.totalTests == 1) { 39 | const resultBar: HTMLDivElement = divById("summary"); 40 | resultBar.classList.remove("NOT_RUN"); 41 | resultBar.classList.remove("FAIL"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${a} 7 | 2 8 | Evaluates the given expression in Python and returns the result. 9 | ${a} = 2 10 | 11 | 12 | 13 | 14 | 15 | ${a} 16 | $a-1 17 | Evaluates the given expression in Python and returns the result. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | All Tests 31 | 32 | 33 | 34 | 35 | Robot5 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | New in 0.0.6 (2022-02-10) 2 | ----------------------------- 3 | 4 | - Fixed issue where stopping logging would not add the start keyword but would end up adding the end keyword. 5 | 6 | 7 | New in 0.0.5 (2022-02-09) 8 | ----------------------------- 9 | 10 | - Fixed one issue generating the output from the standard Robot Framework XML. 11 | - Added API to stop and start logging keywords or keyword arguments. 12 | - Added API to add a string to be hidden from the logs. 13 | - Hiding information through tags in keywords (`log:ignore-variables` and `log:ignore-keywords`). 14 | - Automatically hide the contents of variables named `*password*` or `*passwd*`. 15 | 16 | 17 | New in 0.0.4 (2022-12-13) 18 | ----------------------------- 19 | 20 | - Improved handling in case some internal exception happens. 21 | 22 | 23 | New in 0.0.3 (2022-12-06) 24 | ----------------------------- 25 | 26 | - Properly added `py.typed` file to release. 27 | 28 | 29 | New in 0.0.2 (2022-11-30) 30 | ----------------------------- 31 | 32 | - `LH` message type added to provide embedded html (i.e.: add images to log output). 33 | - `ID` provides an id for the run and the part of the file (incremented when rotating the output). 34 | 35 | - `log.html` improvements: 36 | 37 | - Can filter out keywords with `NOT RUN` status. 38 | - Hides iteration nodes after the 50th iteration (only if marked as `PASS` or `NOT RUN`). 39 | - Embeds HTML contents from log entries with `html=true`. 40 | 41 | 42 | New in 0.0.1 (2022-11-22) 43 | ----------------------------- 44 | 45 | ### First release 46 | 47 | - Note: pre-alpha for early adapters. 48 | - Format may still change. 49 | - Basic structure which allows to memoize strings and build suite/task,test/keyword scope. 50 | - Provides status, time, rotating output, tags, keyword arguments. -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | run-linters: 9 | name: Run linters 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 3.8 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: 3.8 20 | 21 | - name: Setup node 16.x 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 16.x 25 | 26 | - name: Install dev requirements 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install -r dev_requirements.txt 30 | 31 | - name: Install prettier 32 | run: | 33 | npm install -g prettier@2.4.1 34 | 35 | - name: TS Format (yarn prettier --write **/*.ts to format files locally) 36 | working-directory: ./output-webview/ 37 | run: prettier --check **/*.ts 38 | 39 | - name: Black check 40 | run: | 41 | black --check src tests --exclude=vendored --exclude=libs 42 | 43 | - name: mypy create env 44 | run: | 45 | python -m venv .venv 46 | source ./.venv/bin/activate 47 | 48 | python -m pip install -r tests/test_requirements.txt 49 | python -m pip install -r dev_requirements.txt 50 | python -m pip install robotframework 51 | 52 | echo $PWD/src >> .venv/lib/python3.8/site-packages/rf_src.pth 53 | echo $PWD/tests >> .venv/lib/python3.8/site-packages/rf_src.pth 54 | 55 | 56 | - name: mypy robotframework-output-stream 57 | run: | 58 | source ./.venv/bin/activate 59 | mypy --follow-imports=silent --show-column-numbers $PWD/src $PWD/tests 60 | 61 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_while.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot5 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check while 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: BuiltIn 21 | lineno: 3 22 | message_type: SK 23 | name: Evaluate 24 | source: 25 | time_delta_in_seconds: 0 26 | - assign: ${a} 27 | message_type: AS 28 | - argument: '2' 29 | message_type: KA 30 | - level: I 31 | message: ${a} = 2 32 | message_type: L 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - doc: 38 | keyword_type: WHILE 39 | libname: '' 40 | lineno: 4 41 | message_type: SK 42 | name: $a < 1 43 | source: 44 | time_delta_in_seconds: 0 45 | - doc: 46 | keyword_type: ITERATION 47 | libname: '' 48 | lineno: 4 49 | message_type: SK 50 | name: '' 51 | source: 52 | time_delta_in_seconds: 0 53 | - doc: 54 | keyword_type: KEYWORD 55 | libname: BuiltIn 56 | lineno: 5 57 | message_type: SK 58 | name: Evaluate 59 | source: 60 | time_delta_in_seconds: 0 61 | - assign: ${a} 62 | message_type: AS 63 | - argument: $a-1 64 | message_type: KA 65 | - message_type: EK 66 | status: NOT RUN 67 | time_delta_in_seconds: 0 68 | - message_type: EK 69 | status: NOT RUN 70 | time_delta_in_seconds: 0 71 | - message_type: EK 72 | status: NOT RUN 73 | time_delta_in_seconds: 0 74 | - message: '' 75 | message_type: ET 76 | status: PASS 77 | time_delta_in_seconds: 0 78 | - message_type: ES 79 | status: PASS 80 | time_delta_in_seconds: 0 81 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${value} 8 | retval 9 | Returns the given values which can then be assigned to a variables. 10 | ${value} = retval 11 | 12 | 13 | 14 | ${rem} 15 | ${value} 16 | a 17 | Removes all ``removables`` from the given ``string``. 18 | ${rem} = retvl 19 | 20 | 21 | 22 | ${rem} 23 | ${value} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | All Tests 35 | 36 | 37 | 38 | 39 | Robot10 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Does absolutely nothing. 8 | 9 | 10 | 11 | Some error message 12 | level=ERROR 13 | Logs the given message with the given level. 14 | Some error message 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | All Tests 26 | 27 | 28 | 29 | 30 | Robot3 31 | 32 | 33 | 34 | Error in file 'x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\sub\another_sub.robot' on line 2: Library 'lib_not_there.py' does not exist. 35 | Some error message 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_3.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T09:23:53.057' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot4 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot4.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check for 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: '' 23 | keyword_type: FOR 24 | libname: '' 25 | lineno: -1 26 | message_type: SK 27 | name: ${counter} IN RANGE [0 | 1] 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - doc: '' 31 | keyword_type: ITERATION 32 | libname: '' 33 | lineno: -1 34 | message_type: SK 35 | name: ${counter} = 0 36 | source: null 37 | time_delta_in_seconds: -1.0 38 | - doc: Does absolutely nothing. 39 | keyword_type: KEYWORD 40 | libname: BuiltIn 41 | lineno: -1 42 | message_type: SK 43 | name: No Operation 44 | source: null 45 | time_delta_in_seconds: -1.0 46 | - message_type: S 47 | start_time_delta: 0.029 48 | - message_type: EK 49 | status: PASS 50 | time_delta_in_seconds: 0.03 51 | - message_type: S 52 | start_time_delta: 0.029 53 | - message_type: EK 54 | status: PASS 55 | time_delta_in_seconds: 0.03 56 | - message_type: S 57 | start_time_delta: 0.029 58 | - message_type: EK 59 | status: PASS 60 | time_delta_in_seconds: 0.03 61 | - message_type: S 62 | start_time_delta: 0.028 63 | - message: '' 64 | message_type: ET 65 | status: PASS 66 | time_delta_in_seconds: 0.031 67 | - message_type: S 68 | start_time_delta: 0.005 69 | - message_type: ES 70 | status: PASS 71 | time_delta_in_seconds: -1.0 72 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log_args.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog Arguments 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: '' 21 | lineno: 7 22 | message_type: SK 23 | name: Some Keyword 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: log 29 | lineno: 17 30 | message_type: SK 31 | name: Stop Logging Variables 32 | source: 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - doc: 38 | keyword_type: KEYWORD 39 | libname: '' 40 | lineno: 18 41 | message_type: SK 42 | name: Keyword to check 43 | source: 44 | time_delta_in_seconds: 0 45 | - argument: 46 | message_type: KA 47 | - doc: 48 | keyword_type: KEYWORD 49 | libname: BuiltIn 50 | lineno: 13 51 | message_type: SK 52 | name: Log To Console 53 | source: 54 | time_delta_in_seconds: 0 55 | - argument: 56 | message_type: KA 57 | - message_type: EK 58 | status: PASS 59 | time_delta_in_seconds: 0 60 | - message_type: EK 61 | status: PASS 62 | time_delta_in_seconds: 0 63 | - doc: 64 | keyword_type: KEYWORD 65 | libname: log 66 | lineno: 19 67 | message_type: SK 68 | name: Start Logging Variables 69 | source: 70 | time_delta_in_seconds: 0 71 | - message_type: EK 72 | status: PASS 73 | time_delta_in_seconds: 0 74 | - message_type: EK 75 | status: PASS 76 | time_delta_in_seconds: 0 77 | - message: '' 78 | message_type: ET 79 | status: PASS 80 | time_delta_in_seconds: 0 81 | - message_type: ES 82 | status: PASS 83 | time_delta_in_seconds: 0 84 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_2.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T09:16:39.244' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot3 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot3.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 6 18 | message_type: ST 19 | name: First test 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: '' 23 | keyword_type: KEYWORD 24 | libname: another_sub 25 | lineno: -1 26 | message_type: SK 27 | name: Another in sub keyword 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - doc: Does absolutely nothing. 31 | keyword_type: KEYWORD 32 | libname: BuiltIn 33 | lineno: -1 34 | message_type: SK 35 | name: No Operation 36 | source: null 37 | time_delta_in_seconds: -1.0 38 | - message_type: S 39 | start_time_delta: 0.049 40 | - message_type: EK 41 | status: PASS 42 | time_delta_in_seconds: 0.05 43 | - doc: Logs the given message with the given level. 44 | keyword_type: KEYWORD 45 | libname: BuiltIn 46 | lineno: -1 47 | message_type: SK 48 | name: Log 49 | source: null 50 | time_delta_in_seconds: -1.0 51 | - argument: Some error message 52 | message_type: KA 53 | - argument: level=ERROR 54 | message_type: KA 55 | - message_type: S 56 | start_time_delta: 0.05 57 | - message_type: EK 58 | status: PASS 59 | time_delta_in_seconds: 0.052 60 | - message_type: S 61 | start_time_delta: 0.049 62 | - message_type: EK 63 | status: PASS 64 | time_delta_in_seconds: 0.052 65 | - message_type: S 66 | start_time_delta: 0.048 67 | - message: '' 68 | message_type: ET 69 | status: PASS 70 | time_delta_in_seconds: 0.052 71 | - message_type: S 72 | start_time_delta: 0.012 73 | - message_type: ES 74 | status: PASS 75 | time_delta_in_seconds: -1.0 76 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_utilities/test_gen_id.yml: -------------------------------------------------------------------------------- 1 | - a 2 | - b 3 | - c 4 | - d 5 | - e 6 | - f 7 | - g 8 | - h 9 | - i 10 | - j 11 | - k 12 | - l 13 | - m 14 | - n 15 | - o 16 | - p 17 | - q 18 | - r 19 | - s 20 | - t 21 | - u 22 | - v 23 | - w 24 | - x 25 | - y 26 | - z 27 | - A 28 | - B 29 | - C 30 | - D 31 | - E 32 | - F 33 | - G 34 | - H 35 | - I 36 | - J 37 | - K 38 | - L 39 | - M 40 | - N 41 | - O 42 | - P 43 | - Q 44 | - R 45 | - S 46 | - T 47 | - U 48 | - V 49 | - W 50 | - X 51 | - Y 52 | - Z 53 | - '0' 54 | - '1' 55 | - '2' 56 | - '3' 57 | - '4' 58 | - '5' 59 | - '6' 60 | - '7' 61 | - '8' 62 | - '9' 63 | - aa 64 | - ab 65 | - ac 66 | - ad 67 | - ae 68 | - af 69 | - ag 70 | - ah 71 | - ai 72 | - aj 73 | - ak 74 | - al 75 | - am 76 | - an 77 | - ao 78 | - ap 79 | - aq 80 | - ar 81 | - as 82 | - at 83 | - au 84 | - av 85 | - aw 86 | - ax 87 | - ay 88 | - az 89 | - aA 90 | - aB 91 | - aC 92 | - aD 93 | - aE 94 | - aF 95 | - aG 96 | - aH 97 | - aI 98 | - aJ 99 | - aK 100 | - aL 101 | - aM 102 | - aN 103 | - aO 104 | - aP 105 | - aQ 106 | - aR 107 | - aS 108 | - aT 109 | - aU 110 | - aV 111 | - aW 112 | - aX 113 | - aY 114 | - aZ 115 | - a0 116 | - a1 117 | - a2 118 | - a3 119 | - a4 120 | - a5 121 | - a6 122 | - a7 123 | - a8 124 | - a9 125 | - ba 126 | - bb 127 | - bc 128 | - bd 129 | - be 130 | - bf 131 | - bg 132 | - bh 133 | - bi 134 | - bj 135 | - bk 136 | - bl 137 | - bm 138 | - bn 139 | - bo 140 | - bp 141 | - bq 142 | - br 143 | - bs 144 | - bt 145 | - bu 146 | - bv 147 | - bw 148 | - bx 149 | - by 150 | - bz 151 | - bA 152 | - bB 153 | - bC 154 | - bD 155 | - bE 156 | - bF 157 | - bG 158 | - bH 159 | - bI 160 | - bJ 161 | - bK 162 | - bL 163 | - bM 164 | - bN 165 | - bO 166 | - bP 167 | - bQ 168 | - bR 169 | - bS 170 | - bT 171 | - bU 172 | - bV 173 | - bW 174 | - bX 175 | - bY 176 | - bZ 177 | - b0 178 | - b1 179 | - b2 180 | - b3 181 | - b4 182 | - b5 183 | - b6 184 | - b7 185 | - b8 186 | - b9 187 | - ca 188 | - cb 189 | - cc 190 | - cd 191 | - ce 192 | - cf 193 | - cg 194 | - ch 195 | - ci 196 | - cj 197 | - ck 198 | - cl 199 | - cm 200 | - cn 201 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | message 9 | Fails the test with the given message and optionally alters its tags. 10 | message 11 | 12 | 13 | 14 | 15 | 16 | message 17 | 18 | Does absolutely nothing. 19 | 20 | 21 | 22 | 23 | 24 | 25 | Does absolutely nothing. 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | All Tests 39 | 40 | 41 | 42 | 43 | Robot9 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${split} 8 | \#|! 9 | Hello#How are!you 10 | Split the source string by the occurrences of the pattern, 11 | returning a list containing the resulting substrings. If 12 | capturing parentheses are used in pattern, then the text of all 13 | groups in the pattern are also returned as part of the resulting 14 | list. If maxsplit is nonzero, at most maxsplit splits occur, 15 | and the remainder of the string is returned as the final element 16 | of the list. 17 | ${split} = ['Hello', 'How are', 'you'] 18 | 19 | 20 | 21 | ${split} 22 | Logs the given message with the given level. 23 | ['Hello', 'How are', 'you'] 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | All Tests 35 | 36 | 37 | 38 | 39 | Robot Check 40 | Robot Check.Checkmy 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_return.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot10 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 11 14 | message_type: ST 15 | name: Check if 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: '' 21 | lineno: 12 22 | message_type: SK 23 | name: Some Keyword 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: BuiltIn 29 | lineno: 5 30 | message_type: SK 31 | name: Set Variable 32 | source: 33 | time_delta_in_seconds: 0 34 | - assign: ${value} 35 | message_type: AS 36 | - argument: retval 37 | message_type: KA 38 | - level: I 39 | message: ${value} = retval 40 | message_type: L 41 | time_delta_in_seconds: 0 42 | - message_type: EK 43 | status: PASS 44 | time_delta_in_seconds: 0 45 | - doc: 46 | keyword_type: KEYWORD 47 | libname: String 48 | lineno: 6 49 | message_type: SK 50 | name: Remove String 51 | source: 52 | time_delta_in_seconds: 0 53 | - assign: ${rem} 54 | message_type: AS 55 | - argument: ${value} 56 | message_type: KA 57 | - argument: a 58 | message_type: KA 59 | - level: I 60 | message: ${rem} = retvl 61 | message_type: L 62 | time_delta_in_seconds: 0 63 | - message_type: EK 64 | status: PASS 65 | time_delta_in_seconds: 0 66 | - doc: 67 | keyword_type: RETURN 68 | libname: '' 69 | lineno: 7 70 | message_type: SK 71 | name: '' 72 | source: 73 | time_delta_in_seconds: 0 74 | - argument: ${rem} 75 | message_type: KA 76 | - argument: ${value} 77 | message_type: KA 78 | - message_type: EK 79 | status: PASS 80 | time_delta_in_seconds: 0 81 | - message_type: EK 82 | status: PASS 83 | time_delta_in_seconds: 0 84 | - message: '' 85 | message_type: ET 86 | status: PASS 87 | time_delta_in_seconds: 0 88 | - message_type: ES 89 | status: PASS 90 | time_delta_in_seconds: 0 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # IntelliJ 7 | *.iml 8 | *.ipr 9 | *.iws 10 | .idea/ 11 | out/ 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | env3/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | pytest.xml 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # IPython Notebook 80 | .ipynb_checkpoints 81 | .virtual_documents 82 | 83 | # pyenv 84 | .python-version 85 | .venv 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # dotenv 91 | .env 92 | 93 | # virtualenv 94 | venv/ 95 | ENV/ 96 | envs/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # JavaScript 105 | 106 | # vim 107 | *.sw[mnopqrs] 108 | 109 | # Idea 110 | .idea/ 111 | 112 | # Merge orig files 113 | *.orig 114 | 115 | # Special files 116 | .DS_Store 117 | 118 | out 119 | node_modules 120 | 121 | **/log.html 122 | **/output.xml 123 | **/report.html 124 | **/.mypy_cache 125 | 126 | # virtual environment 127 | .venv/ 128 | 129 | 130 | /browser 131 | **/playwright-log.txt 132 | /tests/browser 133 | /tests/playwright-log.txt 134 | /output-webview/dist-test 135 | /output-webview/dist -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log_args_by_tag.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog Args By Tag 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: '' 21 | lineno: 7 22 | message_type: SK 23 | name: Some Keyword 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: '' 29 | lineno: 22 30 | message_type: SK 31 | name: These args should not be logged 32 | source: 33 | time_delta_in_seconds: 0 34 | - argument: 35 | message_type: KA 36 | - doc: 37 | keyword_type: KEYWORD 38 | libname: BuiltIn 39 | lineno: 14 40 | message_type: SK 41 | name: Log To Console 42 | source: 43 | time_delta_in_seconds: 0 44 | - argument: 45 | message_type: KA 46 | - message_type: EK 47 | status: PASS 48 | time_delta_in_seconds: 0 49 | - message_type: EK 50 | status: PASS 51 | time_delta_in_seconds: 0 52 | - message_type: EK 53 | status: PASS 54 | time_delta_in_seconds: 0 55 | - doc: 56 | keyword_type: KEYWORD 57 | libname: '' 58 | lineno: 8 59 | message_type: SK 60 | name: This should be logged 61 | source: 62 | time_delta_in_seconds: 0 63 | - argument: '3' 64 | message_type: KA 65 | - argument: '4' 66 | message_type: KA 67 | - doc: 68 | keyword_type: KEYWORD 69 | libname: BuiltIn 70 | lineno: 18 71 | message_type: SK 72 | name: Log To Console 73 | source: 74 | time_delta_in_seconds: 0 75 | - argument: ${arg1} - ${arg2} 76 | message_type: KA 77 | - message_type: EK 78 | status: PASS 79 | time_delta_in_seconds: 0 80 | - message_type: EK 81 | status: PASS 82 | time_delta_in_seconds: 0 83 | - message: '' 84 | message_type: ET 85 | status: PASS 86 | time_delta_in_seconds: 0 87 | - message_type: ES 88 | status: PASS 89 | time_delta_in_seconds: 0 90 | -------------------------------------------------------------------------------- /output-webview/src/protocols.ts: -------------------------------------------------------------------------------- 1 | import { IMessage } from "./decoder"; 2 | 3 | export interface ITreeState { 4 | openNodes: object; 5 | } 6 | 7 | type IRunIdToTreeState = { 8 | [key: string]: ITreeState; 9 | }; 10 | 11 | export interface IState { 12 | filterLevel: IFilterLevel; 13 | runIdToTreeState: IRunIdToTreeState; 14 | runIdLRU: string[]; 15 | } 16 | 17 | export type IFilterLevel = "FAIL" | "WARN" | "PASS" | "NOT RUN"; 18 | 19 | export interface IOpts { 20 | runId: string; 21 | state: IState | undefined; 22 | onClickReference: Function | undefined; 23 | allRunIdsToLabel: object; 24 | 25 | // Contains the initial file contents. 26 | initialContents: string; 27 | 28 | // Contains the contents added afterwards (i.e.: 29 | // we may add the contents for a session up to a point 30 | // and then add new messages line by line as it's 31 | // being tracked afterwards). 32 | appendedContents: string[]; 33 | } 34 | 35 | export interface ILiNodesCreated { 36 | //
  • 37 | //
    38 | // 39 | // 40 | // 41 | //
    42 | //
  • 43 | li: HTMLLIElement; 44 | details: HTMLDetailsElement; 45 | summary: HTMLElement; 46 | summaryDiv: HTMLDivElement; 47 | span: HTMLElement; 48 | } 49 | 50 | export interface IContentAdded { 51 | //
  • 52 | //
    53 | // 54 | // 55 | // 56 | //
      57 | //
    58 | //
    59 | //
  • 60 | ul: HTMLUListElement; 61 | li: HTMLLIElement; 62 | details: HTMLDetailsElement; 63 | summary: HTMLElement; 64 | summaryDiv: HTMLDivElement; 65 | span: HTMLElement; 66 | source: string; 67 | lineno: number; 68 | 69 | appendContentChild: any; 70 | decodedMessage: IMessage; 71 | 72 | // Updated when the status or level for an element is set (usually at the end). 73 | // When a given item finishes updating it'll update its parent accordingly. 74 | // -1=not run 0 = pass, 1= warn, 2=error 75 | maxLevelFoundInHierarchy: number; 76 | } 77 | 78 | export interface IMessageNode { 79 | message: IMessage; 80 | parent: IMessageNode; 81 | } 82 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_try_except.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot9 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check if 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: TRY 20 | libname: '' 21 | lineno: 3 22 | message_type: SK 23 | name: '' 24 | source: 25 | time_delta_in_seconds: 0 26 | - doc: 27 | keyword_type: KEYWORD 28 | libname: BuiltIn 29 | lineno: 4 30 | message_type: SK 31 | name: Fail 32 | source: 33 | time_delta_in_seconds: 0 34 | - argument: message 35 | message_type: KA 36 | - level: F 37 | message: message 38 | message_type: L 39 | time_delta_in_seconds: 0 40 | - message_type: EK 41 | status: FAIL 42 | time_delta_in_seconds: 0 43 | - message_type: EK 44 | status: FAIL 45 | time_delta_in_seconds: 0 46 | - doc: 47 | keyword_type: EXCEPT 48 | libname: '' 49 | lineno: 5 50 | message_type: SK 51 | name: message 52 | source: 53 | time_delta_in_seconds: 0 54 | - doc: 55 | keyword_type: KEYWORD 56 | libname: BuiltIn 57 | lineno: 6 58 | message_type: SK 59 | name: No Operation 60 | source: 61 | time_delta_in_seconds: 0 62 | - message_type: EK 63 | status: PASS 64 | time_delta_in_seconds: 0 65 | - message_type: EK 66 | status: PASS 67 | time_delta_in_seconds: 0 68 | - doc: 69 | keyword_type: FINALLY 70 | libname: '' 71 | lineno: 7 72 | message_type: SK 73 | name: '' 74 | source: 75 | time_delta_in_seconds: 0 76 | - doc: 77 | keyword_type: KEYWORD 78 | libname: BuiltIn 79 | lineno: 8 80 | message_type: SK 81 | name: No Operation 82 | source: 83 | time_delta_in_seconds: 0 84 | - message_type: EK 85 | status: PASS 86 | time_delta_in_seconds: 0 87 | - message_type: EK 88 | status: PASS 89 | time_delta_in_seconds: 0 90 | - message: '' 91 | message_type: ET 92 | status: PASS 93 | time_delta_in_seconds: 0 94 | - message_type: ES 95 | status: PASS 96 | time_delta_in_seconds: 0 97 | -------------------------------------------------------------------------------- /docs/arguments.md: -------------------------------------------------------------------------------- 1 | # Listener Arguments 2 | 3 | `--dir` 4 | 5 | Points to a directory where the output files should be written. 6 | (default: '.' -- i.e.: working dir). 7 | 8 | Note: if a ':' is used it should be changed to (because a ':' 9 | char is used as the separator by Robot Framework). 10 | So, something as `c:/temp/foo` should be written as `c/temp/foo`. 11 | 12 | Example: 13 | 14 | --dir=./output 15 | --dir=c/temp/output 16 | 17 | `--max-file-size` 18 | 19 | Specifies the maximum file size before a rotation for the output file occurs. 20 | 21 | The size can be specified with its unit. 22 | The following units are supported: `gb, g, mb, m, kb, k, b` 23 | (to support gigabytes=gb or g, megabytes=mb or m, kilobytes=kb or k, bytes=b). 24 | 25 | Note: if no unit is specified, it's considered as bytes. 26 | 27 | Example: 28 | 29 | --max-file-size=200kb 30 | --max-file-size=2mb 31 | --max-file-size=1gb 32 | --max-file-size=10000b 33 | 34 | `--max-files` 35 | 36 | Specifies the maximum number of files to be generated in the logging before 37 | starting to prune old files. 38 | 39 | i.e.: If `--max-files=2`, it will generate `output.rfstream`, `output_2.rfstream` 40 | and when `output_3.rfstream` is about to be generated, it'll erase `output.rfstream`. 41 | 42 | Example: 43 | 44 | --max-files=3 45 | 46 | `--log` 47 | 48 | If specified, writes HTML contents that enable the log contents to be 49 | viewed embedded in an HTML file. 50 | It should point to a path in the filesystem. 51 | 52 | Note: if a ':' is used, it should be changed to (because a ':' 53 | char is used as the separator by Robot Framework). 54 | So, something as `c:/temp/log.html` should be written as `c/temp/log.html`. 55 | 56 | Note: The contents embedded in the file will contain the files written on the disk 57 | but embedded as compressed information (so its size should be less than 58 | the size of the contents on disk), note that contents pruned from the log 59 | (due to the --max-files setting) will NOT appear in the log.html. 60 | 61 | Example: 62 | 63 | --log=./logs/log.html 64 | --log=c/temp/log.html -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml.py: -------------------------------------------------------------------------------- 1 | def convert_in_memory(xml_output): 2 | import io 3 | from robot_out_stream.xml_to_rfstream import convert_xml_to_rfstream 4 | 5 | txt = xml_output.read_text("utf-8") 6 | 7 | source = io.StringIO() 8 | source.write(txt) 9 | source.seek(0) 10 | 11 | in_memory_contents = io.StringIO() 12 | 13 | def write(s): 14 | in_memory_contents.write(s) 15 | 16 | convert_xml_to_rfstream(source, write=write) 17 | in_memory_contents.seek(0) 18 | return in_memory_contents 19 | 20 | 21 | def check(datadir, data_regression, name): 22 | from robot_out_stream import iter_decoded_log_format 23 | 24 | xml_output = datadir / name 25 | rf_stream_output = convert_in_memory(xml_output) 26 | 27 | rf_stream = [] 28 | for line in iter_decoded_log_format(rf_stream_output): 29 | rf_stream.append(line) 30 | 31 | data_regression.check(rf_stream) 32 | for line in rf_stream: 33 | print(line) 34 | 35 | 36 | def test_decode_output_1(datadir, data_regression): 37 | check(datadir, data_regression, "output_1.xml") 38 | 39 | 40 | def test_decode_output_2(datadir, data_regression): 41 | check(datadir, data_regression, "output_2.xml") 42 | 43 | 44 | def test_decode_output_3(datadir, data_regression): 45 | check(datadir, data_regression, "output_3.xml") 46 | 47 | 48 | def test_decode_output_4(datadir, data_regression): 49 | check(datadir, data_regression, "output_4.xml") 50 | 51 | 52 | def test_decode_output_5(datadir, data_regression): 53 | check(datadir, data_regression, "output_5.xml") 54 | 55 | 56 | def test_decode_output_6(datadir, data_regression): 57 | check(datadir, data_regression, "output_6.xml") 58 | 59 | 60 | def test_decode_output_7(datadir, data_regression): 61 | check(datadir, data_regression, "output_7.xml") 62 | 63 | 64 | def test_decode_output_8(datadir, data_regression): 65 | check(datadir, data_regression, "output_8.xml") 66 | 67 | 68 | def test_decode_output_9(datadir, data_regression): 69 | check(datadir, data_regression, "output_9.xml") 70 | 71 | 72 | def test_decode_output_10(datadir, data_regression): 73 | check(datadir, data_regression, "output_10.xml") 74 | 75 | 76 | def test_decode_output_11(datadir, data_regression): 77 | check(datadir, data_regression, "output_11.xml") 78 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/output_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${var1} 7 | 2 8 | Returns the given values which can then be assigned to a variables. 9 | ${var1} = 2 10 | 11 | 12 | 13 | 14 | 15 | Does absolutely nothing. 16 | 17 | 18 | 19 | 20 | 21 | 22 | Does absolutely nothing. 23 | 24 | 25 | 26 | 27 | 28 | 29 | Does absolutely nothing. 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | All Tests 43 | 44 | 45 | 46 | 47 | Robot8 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /output-webview/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Robot Output 6 | 7 | 8 |
    Some text is here
    9 | 13 |
    14 |
    15 | Total:    Failures:     16 |
    17 |
    18 |
    19 | Filter:  20 | 31 |
    32 |
    33 | 38 |
    39 | 40 |
    41 | 42 | 54 | 55 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_6.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T11:34:22.588' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot5 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot5.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check for 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: Evaluates the given expression in Python and returns the result. 23 | keyword_type: KEYWORD 24 | libname: BuiltIn 25 | lineno: -1 26 | message_type: SK 27 | name: Evaluate 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - assign: ${a} 31 | message_type: AS 32 | - argument: '2' 33 | message_type: KA 34 | - level: I 35 | message: ${a} = 2 36 | message_type: L 37 | time_delta_in_seconds: 0.044 38 | - message_type: S 39 | start_time_delta: 0.043 40 | - message_type: EK 41 | status: PASS 42 | time_delta_in_seconds: 0.046 43 | - doc: '' 44 | keyword_type: WHILE 45 | libname: '' 46 | lineno: -1 47 | message_type: SK 48 | name: $a < 1 49 | source: null 50 | time_delta_in_seconds: -1.0 51 | - doc: '' 52 | keyword_type: ITERATION 53 | libname: '' 54 | lineno: -1 55 | message_type: SK 56 | name: '' 57 | source: null 58 | time_delta_in_seconds: -1.0 59 | - doc: Evaluates the given expression in Python and returns the result. 60 | keyword_type: KEYWORD 61 | libname: BuiltIn 62 | lineno: -1 63 | message_type: SK 64 | name: Evaluate 65 | source: null 66 | time_delta_in_seconds: -1.0 67 | - assign: ${a} 68 | message_type: AS 69 | - argument: $a-1 70 | message_type: KA 71 | - message_type: S 72 | start_time_delta: 0.051 73 | - message_type: EK 74 | status: NOT RUN 75 | time_delta_in_seconds: 0.051 76 | - message_type: S 77 | start_time_delta: 0.051 78 | - message_type: EK 79 | status: NOT RUN 80 | time_delta_in_seconds: 0.051 81 | - message_type: S 82 | start_time_delta: 0.046 83 | - message_type: EK 84 | status: NOT RUN 85 | time_delta_in_seconds: 0.051 86 | - message_type: S 87 | start_time_delta: 0.041 88 | - message: '' 89 | message_type: ET 90 | status: PASS 91 | time_delta_in_seconds: 0.052 92 | - message_type: S 93 | start_time_delta: 0.011 94 | - message_type: ES 95 | status: PASS 96 | time_delta_in_seconds: -1.0 97 | -------------------------------------------------------------------------------- /output-webview/src/persistTree.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from "./debounce"; 2 | import { getOpts } from "./options"; 3 | import { divById, getDataTreeId } from "./plainDom"; 4 | import { IState, ITreeState } from "./protocols"; 5 | import { setState } from "./vscodeComm"; 6 | 7 | function collectLITreeState(state: ITreeState, li: HTMLLIElement) { 8 | for (let child of li.childNodes) { 9 | if (child instanceof HTMLDetailsElement) { 10 | for (let c of child.childNodes) { 11 | if (c instanceof HTMLUListElement) { 12 | collectUlTreeState(state, c); 13 | } 14 | } 15 | if (child.open) { 16 | state.openNodes[getDataTreeId(li)] = "open"; 17 | } else { 18 | delete state.openNodes[getDataTreeId(li)]; 19 | } 20 | } 21 | } 22 | } 23 | 24 | function collectUlTreeState(state: ITreeState, ul: HTMLUListElement) { 25 | for (let child of ul.childNodes) { 26 | if (child instanceof HTMLLIElement) { 27 | collectLITreeState(state, child); 28 | } 29 | } 30 | } 31 | 32 | export function collectTreeState(state: IState, runId: string): void { 33 | const mainDiv = divById("mainTree"); 34 | let stateForRun: ITreeState = { "openNodes": {} }; 35 | if (state.runIdLRU === undefined) { 36 | state.runIdLRU = []; 37 | } 38 | if (state.runIdToTreeState === undefined) { 39 | state.runIdToTreeState = {}; 40 | } else { 41 | const oldStateForRun = state.runIdToTreeState[runId]; 42 | if (oldStateForRun) { 43 | // Try to keep previously opened items (even if 44 | // they've been filtered out). 45 | stateForRun = oldStateForRun; 46 | } 47 | } 48 | for (let child of mainDiv.childNodes) { 49 | if (child instanceof HTMLUListElement) { 50 | collectUlTreeState(stateForRun, child); 51 | } 52 | } 53 | 54 | state.runIdToTreeState[runId] = stateForRun; 55 | 56 | // Don't allow it to grow forever... 57 | const index = state.runIdLRU.indexOf(runId); 58 | if (index > -1) { 59 | state.runIdLRU.splice(index, 1); 60 | } 61 | state.runIdLRU.push(runId); 62 | 63 | const MAX_RUNS_SHOWN = 15; 64 | while (state.runIdLRU.length > MAX_RUNS_SHOWN) { 65 | const removeRunId = state.runIdLRU.splice(0, 1); 66 | delete state.runIdToTreeState[removeRunId[0]]; 67 | } 68 | } 69 | 70 | export const saveTreeStateLater = debounce(() => { 71 | saveTreeState(); 72 | }, 500); 73 | 74 | export function saveTreeState() { 75 | const opts = getOpts(); 76 | collectTreeState(opts.state, opts.runId); 77 | setState(opts.state); 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compact and streaming-ready output for Robot Framework. 2 | 3 | A custom output listener for Robot Framework enabling real-time analysis in a more compact format. 4 | 5 | > Note: The current version is still pre-alpha and the [format specified](/docs/format.md) may still change. 6 | 7 | ## Why 8 | 9 | The default Robot Framework output and reports quickly get really big or break when running big or long-running cases. Log handling is also using a lot of resources and if robot execution just breaks, the output.xml usually is corrupted and needs manual fixes. 10 | 11 | ### Scoping the problem 12 | 13 | The default output of Robot Framework (output.xml, log.html and report.html): 14 | * Unable to stream the log by default during the run 15 | * The output generation is a resource-heavy task that can and does break executions. 16 | * `output.xml`, `log.html`, `report.html` get really big on disk. 17 | * The `output.xml` needs "closing", so any problems while creating it yield a corrupted XML file and no logs. 18 | * Post-processing is only possible in some cases and is also resource intensive. 19 | * ..and in cases where the `output.xml` already breaks the post-processing is not possible. 20 | * Flattening and changing the robot code is a big task and is only sometimes possible. 21 | 22 | ### Scoping the solution 23 | We need: 24 | * Compact format to reduce the file size and reduce the load on machine resources. 25 | * The output file that is intact after each write (journaling/transactional pattern). 26 | * Ability to control how much space the logs can take. 27 | * Ability to stream the log during the run 28 | * Match the original data content, so it is possible to create the existing logs from the new data format. 29 | * Solution needs to work without changes to the robot code itself. 30 | 31 | ## What 32 | 33 | Implementation is based on a Robot Framework Listener, so it's possible to use it in any Robot Framework run using the `--listener` argument. 34 | 35 | ### Installation 36 | 37 | Install with: 38 | 39 | `pip install robotframework-output-stream` 40 | 41 | ### Usage 42 | 43 | `python -m robot -l NONE -r NONE -o NONE --listener robot_out_stream.RFStream:--dir=:--max-file-size=<5m>:--max-files=<5>:--log=` 44 | 45 | * The `-l NONE and -r NONE -o NONE` arguments disable the standard Robot Framework output. 46 | * More details on the arguments are below. 47 | 48 | 49 | ## Dealing with sensitive data in the logs 50 | 51 | * See: [Handling Sensitive Data](/docs/handling_sensitive_data.md) 52 | 53 | 54 | ## How 55 | 56 | The "how" of the solution is essentially the listener arguments, data format and the parsers: 57 | 58 | * [Format specification](/docs/format.md) 59 | * [Listener arguments](/docs/arguments.md) 60 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_if.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot8 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 2 14 | message_type: ST 15 | name: Check if 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: BuiltIn 21 | lineno: 3 22 | message_type: SK 23 | name: Set Variable 24 | source: 25 | time_delta_in_seconds: 0 26 | - assign: ${var1} 27 | message_type: AS 28 | - argument: '2' 29 | message_type: KA 30 | - level: I 31 | message: ${var1} = 2 32 | message_type: L 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - doc: 38 | keyword_type: IF 39 | libname: '' 40 | lineno: 4 41 | message_type: SK 42 | name: ${var1} == ${var1} 43 | source: 44 | time_delta_in_seconds: 0 45 | - doc: 46 | keyword_type: KEYWORD 47 | libname: BuiltIn 48 | lineno: 5 49 | message_type: SK 50 | name: No Operation 51 | source: 52 | time_delta_in_seconds: 0 53 | - message_type: EK 54 | status: PASS 55 | time_delta_in_seconds: 0 56 | - message_type: EK 57 | status: PASS 58 | time_delta_in_seconds: 0 59 | - doc: 60 | keyword_type: ELSE IF 61 | libname: '' 62 | lineno: 6 63 | message_type: SK 64 | name: ${var1} == ${var1} 65 | source: 66 | time_delta_in_seconds: 0 67 | - doc: 68 | keyword_type: KEYWORD 69 | libname: BuiltIn 70 | lineno: 7 71 | message_type: SK 72 | name: No Operation 73 | source: 74 | time_delta_in_seconds: 0 75 | - message_type: EK 76 | status: NOT RUN 77 | time_delta_in_seconds: 0 78 | - message_type: EK 79 | status: NOT RUN 80 | time_delta_in_seconds: 0 81 | - doc: 82 | keyword_type: ELSE 83 | libname: '' 84 | lineno: 8 85 | message_type: SK 86 | name: '' 87 | source: 88 | time_delta_in_seconds: 0 89 | - doc: 90 | keyword_type: KEYWORD 91 | libname: BuiltIn 92 | lineno: 9 93 | message_type: SK 94 | name: No Operation 95 | source: 96 | time_delta_in_seconds: 0 97 | - message_type: EK 98 | status: NOT RUN 99 | time_delta_in_seconds: 0 100 | - message_type: EK 101 | status: NOT RUN 102 | time_delta_in_seconds: 0 103 | - message: '' 104 | message_type: ET 105 | status: PASS 106 | time_delta_in_seconds: 0 107 | - message_type: ES 108 | status: PASS 109 | time_delta_in_seconds: 0 110 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_1.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T07:45:57.116' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0.1 (Python 3.7.5 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot Check 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\local_test\robot_check 16 | time_delta_in_seconds: -1.0 17 | - message_type: SS 18 | name: Checkmy 19 | suite_id: s1-s1 20 | suite_source: x:\vscode-robot\local_test\robot_check\checkmy.robot 21 | time_delta_in_seconds: -1.0 22 | - lineno: 5 23 | message_type: ST 24 | name: My test 25 | suite_id: s1-s1-t1 26 | time_delta_in_seconds: -1.0 27 | - doc: 'Split the source string by the occurrences of the pattern, 28 | 29 | returning a list containing the resulting substrings. If 30 | 31 | capturing parentheses are used in pattern, then the text of all 32 | 33 | groups in the pattern are also returned as part of the resulting 34 | 35 | list. If maxsplit is nonzero, at most maxsplit splits occur, 36 | 37 | and the remainder of the string is returned as the final element 38 | 39 | of the list.' 40 | keyword_type: KEYWORD 41 | libname: re 42 | lineno: -1 43 | message_type: SK 44 | name: Split 45 | source: null 46 | time_delta_in_seconds: -1.0 47 | - assign: ${split} 48 | message_type: AS 49 | - argument: \#|! 50 | message_type: KA 51 | - argument: Hello#How are!you 52 | message_type: KA 53 | - level: I 54 | message: ${split} = ['Hello', 'How are', 'you'] 55 | message_type: L 56 | time_delta_in_seconds: 0.055 57 | - message_type: S 58 | start_time_delta: 0.055 59 | - message_type: EK 60 | status: PASS 61 | time_delta_in_seconds: 0.057 62 | - doc: Logs the given message with the given level. 63 | keyword_type: KEYWORD 64 | libname: BuiltIn 65 | lineno: -1 66 | message_type: SK 67 | name: Log 68 | source: null 69 | time_delta_in_seconds: -1.0 70 | - argument: ${split} 71 | message_type: KA 72 | - level: I 73 | message: '[''Hello'', ''How are'', ''you'']' 74 | message_type: L 75 | time_delta_in_seconds: 0.059 76 | - message_type: S 77 | start_time_delta: 0.058 78 | - message_type: EK 79 | status: PASS 80 | time_delta_in_seconds: 0.059 81 | - message_type: S 82 | start_time_delta: 0.053 83 | - message: '' 84 | message_type: ET 85 | status: PASS 86 | time_delta_in_seconds: 0.059 87 | - message_type: S 88 | start_time_delta: 0.043 89 | - message_type: ES 90 | status: PASS 91 | time_delta_in_seconds: -1.0 92 | - message_type: S 93 | start_time_delta: 0.007 94 | - message_type: ES 95 | status: PASS 96 | time_delta_in_seconds: -1.0 97 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_impl_recovery.py: -------------------------------------------------------------------------------- 1 | def test_impl_recovery_matches_suite(): 2 | from robot_out_stream._impl import _RobotOutputImpl, _Config 3 | 4 | config = _Config("uuid") 5 | config.output_dir = None 6 | config.max_file_size_in_bytes = 9999999999 7 | config.max_files = 1 8 | config.log_html = None 9 | impl = _RobotOutputImpl(config) 10 | impl.start_suite("My Suite", "suite1", "", 0) 11 | impl.start_test("My Test", "test1", 0, 0, []) 12 | assert len(impl._stack_handler._queue) == 2 13 | 14 | # Unsynchronized end suite (clear until we reach it). 15 | impl.end_suite("suite1", "PASS", 0) 16 | 17 | assert len(impl._stack_handler._queue) == 0 18 | 19 | 20 | def test_impl_recovery_matches_test(): 21 | from robot_out_stream._impl import _RobotOutputImpl, _Config 22 | 23 | config = _Config("uuid") 24 | config.output_dir = None 25 | config.max_file_size_in_bytes = 9999999999 26 | config.max_files = 1 27 | config.log_html = None 28 | impl = _RobotOutputImpl(config) 29 | impl.start_suite("My Suite", "suite1", "", 0) 30 | impl.start_test("My Test", "test1", 0, 0, []) 31 | impl.start_keyword( 32 | "My Keyword", "libname", "KEYWORD", "doc", "source", 0, 0, [], [] 33 | ) 34 | assert len(impl._stack_handler._queue) == 3 35 | 36 | # Unsynchronized end test (clear until we reach it). 37 | impl.end_test("test1", "PASS", "", 0) 38 | assert len(impl._stack_handler._queue) == 1 39 | impl.end_suite("suite1", "PASS", 0) 40 | assert len(impl._stack_handler._queue) == 0 41 | 42 | 43 | def test_impl_recovery_does_not_match_test(): 44 | from robot_out_stream._impl import _RobotOutputImpl, _Config 45 | 46 | config = _Config("uuid") 47 | config.output_dir = None 48 | config.max_file_size_in_bytes = 9999999999 49 | config.max_files = 1 50 | config.log_html = None 51 | impl = _RobotOutputImpl(config) 52 | impl.start_suite("My Suite", "suite1", "", 0) 53 | impl.start_test("My Test", "test1", 0, 0, []) 54 | impl.start_keyword( 55 | "My Keyword", "libname", "KEYWORD", "doc", "source", 0, 0, [], [] 56 | ) 57 | assert len(impl._stack_handler._queue) == 3 58 | 59 | # Unsynchronized end test (clear all keywords). 60 | impl.end_test("no-match", "PASS", "", 0) 61 | assert len(impl._stack_handler._queue) == 1 62 | impl.end_suite("suite1", "PASS", 0) 63 | assert len(impl._stack_handler._queue) == 0 64 | 65 | 66 | def test_impl_recovery_do_nothing(): 67 | from robot_out_stream._impl import _RobotOutputImpl, _Config 68 | 69 | config = _Config("uuid") 70 | config.output_dir = None 71 | config.max_file_size_in_bytes = 9999999999 72 | config.max_files = 1 73 | config.log_html = None 74 | impl = _RobotOutputImpl(config) 75 | impl.end_suite("suite1", "PASS", 0) 76 | assert len(impl._stack_handler._queue) == 0 77 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_10.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-11-03T08:31:39.003' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot10 14 | suite_id: s1 15 | suite_source: d:\x\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot10.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 11 18 | message_type: ST 19 | name: Check if 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: '' 23 | keyword_type: KEYWORD 24 | libname: '' 25 | lineno: -1 26 | message_type: SK 27 | name: Some Keyword 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - doc: Returns the given values which can then be assigned to a variables. 31 | keyword_type: KEYWORD 32 | libname: BuiltIn 33 | lineno: -1 34 | message_type: SK 35 | name: Set Variable 36 | source: null 37 | time_delta_in_seconds: -1.0 38 | - assign: ${value} 39 | message_type: AS 40 | - argument: retval 41 | message_type: KA 42 | - level: I 43 | message: ${value} = retval 44 | message_type: L 45 | time_delta_in_seconds: 0.038 46 | - message_type: S 47 | start_time_delta: 0.038 48 | - message_type: EK 49 | status: PASS 50 | time_delta_in_seconds: 0.04 51 | - doc: Removes all ``removables`` from the given ``string``. 52 | keyword_type: KEYWORD 53 | libname: String 54 | lineno: -1 55 | message_type: SK 56 | name: Remove String 57 | source: null 58 | time_delta_in_seconds: -1.0 59 | - assign: ${rem} 60 | message_type: AS 61 | - argument: ${value} 62 | message_type: KA 63 | - argument: a 64 | message_type: KA 65 | - level: I 66 | message: ${rem} = retvl 67 | message_type: L 68 | time_delta_in_seconds: 0.041 69 | - message_type: S 70 | start_time_delta: 0.04 71 | - message_type: EK 72 | status: PASS 73 | time_delta_in_seconds: 0.041 74 | - doc: '' 75 | keyword_type: RETURN 76 | libname: '' 77 | lineno: -1 78 | message_type: SK 79 | name: ${rem} | ${value} 80 | source: null 81 | time_delta_in_seconds: -1.0 82 | - message_type: S 83 | start_time_delta: 0.041 84 | - message_type: EK 85 | status: PASS 86 | time_delta_in_seconds: 0.041 87 | - message_type: S 88 | start_time_delta: 0.038 89 | - message_type: EK 90 | status: PASS 91 | time_delta_in_seconds: 0.041 92 | - message_type: S 93 | start_time_delta: 0.037 94 | - message: '' 95 | message_type: ET 96 | status: PASS 97 | time_delta_in_seconds: 0.042 98 | - message_type: S 99 | start_time_delta: 0.005 100 | - message_type: ES 101 | status: PASS 102 | time_delta_in_seconds: -1.0 103 | -------------------------------------------------------------------------------- /src/setup.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | _here = Path(__file__).parent.resolve() 7 | _parent = _here.parent 8 | _root = _parent.parent 9 | 10 | _readme = _here / "README.md" 11 | _thirdparty = _here / "ThirdPartyNotices.txt" 12 | _license = _here / "LICENSE" 13 | _copyright = _here / "COPYRIGHT" 14 | 15 | # Note: always overwrite files from the original place if those exist. 16 | 17 | _origin = _parent / _readme.name 18 | if _origin.exists(): 19 | shutil.copy2(_origin, _readme) 20 | 21 | for path in [_thirdparty, _license, _copyright]: 22 | _origin = _root / path.name 23 | if _origin.exists(): 24 | shutil.copy2(_origin, path) 25 | 26 | setup( 27 | name="robotframework-output-stream", 28 | version="0.0.6", 29 | description="Robot Framework Output Stream (a RF listener to provide an output that's compact and streamable).", 30 | long_description=_readme.read_text(), 31 | url="https://github.com/robocorp/robotframework-output-stream", 32 | author="Fabio Zadrozny", 33 | license="Apache-2.0", 34 | copyright="Robocorp Technologies, Inc.", 35 | packages=find_packages(), 36 | package_data={"robot_out_stream": ["py.typed"]}, 37 | zip_safe=False, 38 | long_description_content_type="text/markdown", 39 | python_requires=">=3.7", 40 | # List run-time dependencies here. These will be installed by pip when 41 | # your project is installed. For an analysis of "install_requires" vs pip's 42 | # requirements files see: 43 | # https://packaging.python.org/en/latest/requirements.html 44 | install_requires=["robotframework >=3.2"], 45 | # List additional groups of dependencies here (e.g. development 46 | # dependencies). You can install these using the following syntax, 47 | # for example: 48 | # $ pip install -e .[test] 49 | extras_require={ 50 | "test": [ 51 | "mock", 52 | "pytest", 53 | "pytest-regressions==1.0.6", 54 | "pytest-xdist", 55 | "pytest-timeout", 56 | ], 57 | }, 58 | classifiers=[ 59 | # "Development Status :: 5 - Production/Stable", 60 | "Development Status :: 2 - Pre-Alpha", 61 | "Environment :: Console", 62 | "Intended Audience :: Developers", 63 | "License :: OSI Approved :: Apache Software License", 64 | "Operating System :: MacOS :: MacOS X", 65 | "Operating System :: Microsoft :: Windows", 66 | "Operating System :: POSIX", 67 | "Programming Language :: Python", 68 | "Programming Language :: Python :: 3.7", 69 | "Programming Language :: Python :: 3.8", 70 | "Programming Language :: Python :: 3.9", 71 | "Programming Language :: Python :: 3.10", 72 | "Programming Language :: Python :: 3.11", 73 | "Framework :: Robot Framework", 74 | "Framework :: Robot Framework :: Tool", 75 | ], 76 | ) 77 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_robot_stream/test_robot_no_log_args_by_name.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T10:00:00.000' 4 | message_type: T 5 | - id: 1234-uuid-in-tests 6 | message_type: ID 7 | part: 1 8 | - message_type: SS 9 | name: Robot Nolog Args By Name 10 | suite_id: s1 11 | suite_source: 12 | time_delta_in_seconds: 0 13 | - lineno: 6 14 | message_type: ST 15 | name: Check no log 16 | suite_id: s1-t1 17 | time_delta_in_seconds: 0 18 | - doc: 19 | keyword_type: KEYWORD 20 | libname: BuiltIn 21 | lineno: 8 22 | message_type: SK 23 | name: Set Variable 24 | source: 25 | time_delta_in_seconds: 0 26 | - assign: ${password} 27 | message_type: AS 28 | - argument: 29 | message_type: KA 30 | - level: I 31 | message: ${password} = 32 | message_type: L 33 | time_delta_in_seconds: 0 34 | - message_type: EK 35 | status: PASS 36 | time_delta_in_seconds: 0 37 | - doc: 38 | keyword_type: KEYWORD 39 | libname: '' 40 | lineno: 9 41 | message_type: SK 42 | name: Some Keyword 43 | source: 44 | time_delta_in_seconds: 0 45 | - argument: ${password} 46 | message_type: KA 47 | - doc: 48 | keyword_type: KEYWORD 49 | libname: BuiltIn 50 | lineno: 19 51 | message_type: SK 52 | name: Log 53 | source: 54 | time_delta_in_seconds: 0 55 | - argument: ${password} 56 | message_type: KA 57 | - level: I 58 | message: 59 | message_type: L 60 | time_delta_in_seconds: 0 61 | - message_type: EK 62 | status: PASS 63 | time_delta_in_seconds: 0 64 | - message_type: EK 65 | status: PASS 66 | time_delta_in_seconds: 0 67 | - doc: 68 | keyword_type: KEYWORD 69 | libname: log 70 | lineno: 12 71 | message_type: SK 72 | name: Hide From Output 73 | source: 74 | time_delta_in_seconds: 0 75 | - argument: 76 | message_type: KA 77 | - message_type: EK 78 | status: PASS 79 | time_delta_in_seconds: 0 80 | - doc: 81 | keyword_type: KEYWORD 82 | libname: '' 83 | lineno: 13 84 | message_type: SK 85 | name: Some Keyword 86 | source: 87 | time_delta_in_seconds: 0 88 | - argument: 89 | message_type: KA 90 | - doc: 91 | keyword_type: KEYWORD 92 | libname: BuiltIn 93 | lineno: 19 94 | message_type: SK 95 | name: Log 96 | source: 97 | time_delta_in_seconds: 0 98 | - argument: ${password} 99 | message_type: KA 100 | - level: I 101 | message: 102 | message_type: L 103 | time_delta_in_seconds: 0 104 | - message_type: EK 105 | status: PASS 106 | time_delta_in_seconds: 0 107 | - message_type: EK 108 | status: PASS 109 | time_delta_in_seconds: 0 110 | - message: '' 111 | message_type: ET 112 | status: PASS 113 | time_delta_in_seconds: 0 114 | - message_type: ES 115 | status: PASS 116 | time_delta_in_seconds: 0 117 | -------------------------------------------------------------------------------- /.github/workflows/tests-robot-out-stream.yml: -------------------------------------------------------------------------------- 1 | name: Tests - Robot Output Stream (robotframework-output-stream) 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | name: [ 13 | "windows-py37-pip-v3", 14 | "ubuntu-py38-master-outviewintegrationtests", 15 | "ubuntu-py38-pip-v4", 16 | ] 17 | 18 | include: 19 | - name: "windows-py37-pip-v3" 20 | python: "3.7" 21 | os: windows-latest 22 | - name: "ubuntu-py38-master-outviewintegrationtests" 23 | python: "3.8" 24 | os: ubuntu-latest 25 | - name: "ubuntu-py38-pip-v4" 26 | python: "3.8" 27 | os: ubuntu-latest 28 | 29 | 30 | steps: 31 | - name: Checkout repository and submodules 32 | uses: actions/checkout@v2 33 | - name: Set up Python ${{ matrix.python }} 34 | uses: actions/setup-python@v1 35 | with: 36 | python-version: ${{ matrix.python }} 37 | - name: Setup node 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: 16.x 41 | - name: Upgrade pip 42 | run: python -m pip install --upgrade pip 43 | - name: Install robotframework from master 44 | run: python -W ignore -m pip install https://github.com/robotframework/robotframework/archive/master.zip --no-warn-script-location --disable-pip-version-check 45 | if: contains(matrix.name, '-master') 46 | - name: Install robotframework from pip 47 | run: python -W ignore -m pip install "robotframework>=3.2,<4.0" 48 | if: contains(matrix.name, '-pip-v3') 49 | - name: Install robotframework from pip 50 | run: python -W ignore -m pip install "robotframework>=4.0" 51 | if: contains(matrix.name, '-pip-v4') 52 | - name: setup.py install 53 | run: | 54 | cd src 55 | python setup.py install 56 | cd .. 57 | - name: Install test deps 58 | run: | 59 | cd tests 60 | pip install -r test_requirements.txt 61 | pip install fire 62 | cd .. 63 | - name: Embed output view in index.py 64 | run: | 65 | python -m dev build-output-view 66 | # python -m dev check-no-git-changes -- disabled because 67 | # building in prod makes changes appear even if the entry 68 | # code is the same. 69 | - name: Build output view 70 | if: contains(matrix.name, '-outviewintegrationtests') 71 | working-directory: ./output-webview 72 | run: | 73 | yarn install 74 | yarn build-test 75 | - name: Test 76 | working-directory: ./tests 77 | env: 78 | PYTHONPATH: . 79 | RUN_TESTS_TIMEOUT: 10000 80 | GITHUB_ACTIONS_MATRIX_NAME: ${{ matrix.name }} 81 | run: python -u -m pytest -rfE -vv . 82 | - uses: actions/upload-artifact@v3 83 | if: contains(matrix.name, '-outviewintegrationtests') 84 | with: 85 | name: robotlog.${{ matrix.name }}.html 86 | path: output-webview/tests/output/log.html 87 | 88 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_8.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T14:53:07.510' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot9 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot9.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check if 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: '' 23 | keyword_type: TRY 24 | libname: '' 25 | lineno: -1 26 | message_type: SK 27 | name: '' 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - doc: Fails the test with the given message and optionally alters its tags. 31 | keyword_type: KEYWORD 32 | libname: BuiltIn 33 | lineno: -1 34 | message_type: SK 35 | name: Fail 36 | source: null 37 | time_delta_in_seconds: -1.0 38 | - argument: message 39 | message_type: KA 40 | - level: F 41 | message: message 42 | message_type: L 43 | time_delta_in_seconds: 0.038 44 | - message_type: S 45 | start_time_delta: 0.038 46 | - message_type: EK 47 | status: FAIL 48 | time_delta_in_seconds: 0.04 49 | - message_type: S 50 | start_time_delta: 0.038 51 | - message_type: EK 52 | status: FAIL 53 | time_delta_in_seconds: 0.04 54 | - doc: '' 55 | keyword_type: EXCEPT 56 | libname: '' 57 | lineno: -1 58 | message_type: SK 59 | name: message 60 | source: null 61 | time_delta_in_seconds: -1.0 62 | - doc: Does absolutely nothing. 63 | keyword_type: KEYWORD 64 | libname: BuiltIn 65 | lineno: -1 66 | message_type: SK 67 | name: No Operation 68 | source: null 69 | time_delta_in_seconds: -1.0 70 | - message_type: S 71 | start_time_delta: 0.04 72 | - message_type: EK 73 | status: PASS 74 | time_delta_in_seconds: 0.041 75 | - message_type: S 76 | start_time_delta: 0.04 77 | - message_type: EK 78 | status: PASS 79 | time_delta_in_seconds: 0.041 80 | - doc: '' 81 | keyword_type: FINALLY 82 | libname: '' 83 | lineno: -1 84 | message_type: SK 85 | name: '' 86 | source: null 87 | time_delta_in_seconds: -1.0 88 | - doc: Does absolutely nothing. 89 | keyword_type: KEYWORD 90 | libname: BuiltIn 91 | lineno: -1 92 | message_type: SK 93 | name: No Operation 94 | source: null 95 | time_delta_in_seconds: -1.0 96 | - message_type: S 97 | start_time_delta: 0.041 98 | - message_type: EK 99 | status: PASS 100 | time_delta_in_seconds: 0.041 101 | - message_type: S 102 | start_time_delta: 0.041 103 | - message_type: EK 104 | status: PASS 105 | time_delta_in_seconds: 0.041 106 | - message_type: S 107 | start_time_delta: 0.037 108 | - message: '' 109 | message_type: ET 110 | status: PASS 111 | time_delta_in_seconds: 0.042 112 | - message_type: S 113 | start_time_delta: 0.009 114 | - message_type: ES 115 | status: PASS 116 | time_delta_in_seconds: -1.0 117 | -------------------------------------------------------------------------------- /output-webview/src/plainDom.ts: -------------------------------------------------------------------------------- 1 | function byId(id: string): type { 2 | return document.getElementById(id); 3 | } 4 | 5 | export function divById(id: string): HTMLDivElement { 6 | return byId(id); 7 | } 8 | 9 | export function selectById(id: string): HTMLSelectElement { 10 | return byId(id); 11 | } 12 | 13 | export function createUL(id: string): HTMLUListElement { 14 | const element = document.createElement("ul"); 15 | element.setAttribute("data-tree-id", id); 16 | return element; 17 | } 18 | 19 | export function createSummary(): HTMLElement { 20 | return document.createElement("summary"); 21 | } 22 | 23 | export function createSpan(): HTMLSpanElement { 24 | return document.createElement("span"); 25 | } 26 | 27 | export function createOption(): HTMLOptionElement { 28 | return document.createElement("option"); 29 | } 30 | 31 | export function createButton(): HTMLButtonElement { 32 | return document.createElement("button"); 33 | } 34 | 35 | export function createDiv(): HTMLDivElement { 36 | return document.createElement("div"); 37 | } 38 | 39 | export function createLI(id: string): HTMLLIElement { 40 | const element = document.createElement("li"); 41 | element.setAttribute("data-tree-id", id); 42 | return element; 43 | } 44 | 45 | export function createDetails(): HTMLDetailsElement { 46 | return document.createElement("details"); 47 | } 48 | 49 | export function getDataTreeId(element: HTMLLIElement | HTMLUListElement) { 50 | return element.getAttribute("data-tree-id"); 51 | } 52 | 53 | export function liMarkedAsHidden( 54 | element: HTMLLIElement | HTMLUListElement, 55 | markAsHidden: boolean | undefined = undefined 56 | ) { 57 | if (markAsHidden === undefined) { 58 | // getter 59 | return element.getAttribute("data-hidden") === "1"; 60 | } 61 | // setter 62 | if (markAsHidden) { 63 | element.setAttribute("data-hidden", "1"); 64 | } else { 65 | element.removeAttribute("data-hidden"); 66 | } 67 | } 68 | 69 | export function htmlToElement(html) { 70 | var template = document.createElement("template"); 71 | html = html.trim(); // Never return a text node of whitespace as the result 72 | template.innerHTML = html; 73 | return template.content.firstChild; 74 | } 75 | 76 | // codicon: https://icon-sets.iconify.design/codicon/expand-all/ 77 | export function createExpandSVG() { 78 | return htmlToElement( 79 | `` 80 | ); 81 | } 82 | 83 | // codicon: https://icon-sets.iconify.design/codicon/collapse-all/ 84 | export function createCollapseSVG() { 85 | return htmlToElement( 86 | `` 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /output-webview/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getOpts } from "./options"; 2 | import { saveTreeState } from "./persistTree"; 3 | import { selectById } from "./plainDom"; 4 | import { IFilterLevel } from "./protocols"; 5 | import { rebuildRunSelection } from "./runSelection"; 6 | import { getSampleContents } from "./sample"; 7 | import "./style.css"; 8 | import { TreeBuilder } from "./treeBuilder"; 9 | import { 10 | requestToHandler, 11 | sendEventToClient, 12 | nextMessageSeq, 13 | IEventMessage, 14 | ISetContentsRequest, 15 | IAppendContentsRequest, 16 | IUpdateLabelRequest, 17 | } from "./vscodeComm"; 18 | 19 | let treeBuilder: TreeBuilder | undefined; 20 | 21 | async function rebuildTreeAndStatusesFromOpts(): Promise { 22 | treeBuilder = new TreeBuilder(); 23 | treeBuilder.clearAndInitializeTree(); 24 | await treeBuilder.addInitialContents(); 25 | } 26 | 27 | export function updateFilterLevel(filterLevel: IFilterLevel) { 28 | const opts = getOpts(); 29 | if (opts.state.filterLevel !== filterLevel) { 30 | opts.state.filterLevel = filterLevel; 31 | saveTreeState(); 32 | rebuildTreeAndStatusesFromOpts(); 33 | } 34 | } 35 | 36 | function onClickReference(message) { 37 | let ev: IEventMessage = { 38 | type: "event", 39 | seq: nextMessageSeq(), 40 | event: "onClickReference", 41 | }; 42 | ev["data"] = message; 43 | sendEventToClient(ev); 44 | } 45 | 46 | /** 47 | * Should be called to set the initial contents of the tree 48 | * as well as the current run id and label. 49 | */ 50 | export function setContents(msg: ISetContentsRequest): void { 51 | saveTreeState(); 52 | const opts = getOpts(); 53 | opts.runId = msg.runId; 54 | opts.initialContents = msg.initialContents; 55 | opts.onClickReference = onClickReference; 56 | opts.appendedContents = []; 57 | opts.allRunIdsToLabel = msg.allRunIdsToLabel; 58 | 59 | rebuildRunSelection(opts.allRunIdsToLabel, opts.runId); 60 | rebuildTreeAndStatusesFromOpts(); 61 | } 62 | 63 | export function appendContents(msg: IAppendContentsRequest): void { 64 | const opts = getOpts(); 65 | if (opts.runId === msg.runId) { 66 | opts.appendedContents.push(msg.appendContents); 67 | if (treeBuilder !== undefined) { 68 | treeBuilder.onAppendedContents(); 69 | } 70 | } 71 | } 72 | 73 | export function updateLabel(msg: IUpdateLabelRequest): void { 74 | const opts = getOpts(); 75 | opts.allRunIdsToLabel[msg.runId] = msg.label; 76 | rebuildRunSelection(opts.allRunIdsToLabel, opts.runId); 77 | } 78 | 79 | requestToHandler["setContents"] = setContents; 80 | requestToHandler["appendContents"] = appendContents; 81 | requestToHandler["updateLabel"] = updateLabel; 82 | 83 | function onChangedFilterLevel() { 84 | const filterLevel = selectById("filterLevel"); 85 | const value: IFilterLevel = (filterLevel).value; 86 | updateFilterLevel(value); 87 | } 88 | 89 | function onChangedRun() { 90 | const selectedRun = selectById("selectedRun").value; 91 | let ev: IEventMessage = { 92 | type: "event", 93 | seq: nextMessageSeq(), 94 | event: "onSetCurrentRunId", 95 | }; 96 | ev["data"] = { "runId": selectedRun }; 97 | sendEventToClient(ev); 98 | } 99 | 100 | window["onChangedRun"] = onChangedRun; 101 | window["onChangedFilterLevel"] = onChangedFilterLevel; 102 | window["setContents"] = setContents; 103 | window["getSampleContents"] = getSampleContents; 104 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/test_decode_xml/test_decode_output_7.yml: -------------------------------------------------------------------------------- 1 | - message_type: V 2 | version: '1' 3 | - initial_time: '2022-10-31T14:45:57.829' 4 | message_type: T 5 | - id: gen-from-output-xml 6 | message_type: ID 7 | part: 1 8 | - info: Generated from output.xml 9 | message_type: I 10 | - info: Robot 5.0rc1 (Python 3.8.1 on win32) 11 | message_type: I 12 | - message_type: SS 13 | name: Robot8 14 | suite_id: s1 15 | suite_source: x:\vscode-robot\robotframework-lsp\robotframework-output-stream\tests\robot_out_stream_tests\test_robot_out_stream\robot8.robot 16 | time_delta_in_seconds: -1.0 17 | - lineno: 2 18 | message_type: ST 19 | name: Check if 20 | suite_id: s1-t1 21 | time_delta_in_seconds: -1.0 22 | - doc: Returns the given values which can then be assigned to a variables. 23 | keyword_type: KEYWORD 24 | libname: BuiltIn 25 | lineno: -1 26 | message_type: SK 27 | name: Set Variable 28 | source: null 29 | time_delta_in_seconds: -1.0 30 | - assign: ${var1} 31 | message_type: AS 32 | - argument: '2' 33 | message_type: KA 34 | - level: I 35 | message: ${var1} = 2 36 | message_type: L 37 | time_delta_in_seconds: 0.046 38 | - message_type: S 39 | start_time_delta: 0.045 40 | - message_type: EK 41 | status: PASS 42 | time_delta_in_seconds: 0.05 43 | - doc: '' 44 | keyword_type: IF 45 | libname: '' 46 | lineno: -1 47 | message_type: SK 48 | name: ${var1} == ${var1} 49 | source: null 50 | time_delta_in_seconds: -1.0 51 | - doc: Does absolutely nothing. 52 | keyword_type: KEYWORD 53 | libname: BuiltIn 54 | lineno: -1 55 | message_type: SK 56 | name: No Operation 57 | source: null 58 | time_delta_in_seconds: -1.0 59 | - message_type: S 60 | start_time_delta: 0.052 61 | - message_type: EK 62 | status: PASS 63 | time_delta_in_seconds: 0.053 64 | - message_type: S 65 | start_time_delta: 0.051 66 | - message_type: EK 67 | status: PASS 68 | time_delta_in_seconds: 0.053 69 | - doc: '' 70 | keyword_type: ELSE IF 71 | libname: '' 72 | lineno: -1 73 | message_type: SK 74 | name: ${var1} == ${var1} 75 | source: null 76 | time_delta_in_seconds: -1.0 77 | - doc: Does absolutely nothing. 78 | keyword_type: KEYWORD 79 | libname: BuiltIn 80 | lineno: -1 81 | message_type: SK 82 | name: No Operation 83 | source: null 84 | time_delta_in_seconds: -1.0 85 | - message_type: S 86 | start_time_delta: 0.053 87 | - message_type: EK 88 | status: NOT RUN 89 | time_delta_in_seconds: 0.054 90 | - message_type: S 91 | start_time_delta: 0.053 92 | - message_type: EK 93 | status: NOT RUN 94 | time_delta_in_seconds: 0.054 95 | - doc: '' 96 | keyword_type: ELSE 97 | libname: '' 98 | lineno: -1 99 | message_type: SK 100 | name: '' 101 | source: null 102 | time_delta_in_seconds: -1.0 103 | - doc: Does absolutely nothing. 104 | keyword_type: KEYWORD 105 | libname: BuiltIn 106 | lineno: -1 107 | message_type: SK 108 | name: No Operation 109 | source: null 110 | time_delta_in_seconds: -1.0 111 | - message_type: S 112 | start_time_delta: 0.054 113 | - message_type: EK 114 | status: NOT RUN 115 | time_delta_in_seconds: 0.055 116 | - message_type: S 117 | start_time_delta: 0.054 118 | - message_type: EK 119 | status: NOT RUN 120 | time_delta_in_seconds: 0.055 121 | - message_type: S 122 | start_time_delta: 0.044 123 | - message: '' 124 | message_type: ET 125 | status: PASS 126 | time_delta_in_seconds: 0.056 127 | - message_type: S 128 | start_time_delta: 0.018 129 | - message_type: ES 130 | status: PASS 131 | time_delta_in_seconds: -1.0 132 | -------------------------------------------------------------------------------- /output-webview/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const HtmlInlineScriptPlugin = require("html-inline-script-webpack-plugin"); 4 | 5 | module.exports = (env) => { 6 | let mode; 7 | let devtool; 8 | let minimize; 9 | 10 | if (env.production) { 11 | console.log("Building in PRODUCTION mode!"); 12 | 13 | mode = "production"; 14 | devtool = false; 15 | minimize = true; 16 | } else if (env.development) { 17 | console.log("Building in DEV mode!"); 18 | mode = "development"; 19 | // devtool = 'cheap-module-source-map'; 20 | // devtool = 'eval'; 21 | devtool = "source-map"; 22 | minimize = false; 23 | } else if (env.test) { 24 | console.log("Building in TEST mode!"); 25 | mode = "development"; 26 | // devtool = 'cheap-module-source-map'; 27 | devtool = "eval-source-map"; 28 | minimize = false; 29 | } else { 30 | throw Error("Either production or development need to be specified"); 31 | } 32 | 33 | target = path.resolve(__dirname, "dist"); 34 | if (env.test) { 35 | target = path.resolve(__dirname, "dist-test"); 36 | } 37 | 38 | let envTarget = env.target; 39 | if (envTarget) { 40 | target = envTarget; 41 | } 42 | console.log("Building to: " + target); 43 | 44 | let plugins = [ 45 | // Generates the index.html 46 | new HtmlWebpackPlugin({ 47 | title: "Robot Output", 48 | template: "./src/index.html", 49 | scriptLoading: "blocking", 50 | }), 51 | ]; 52 | 53 | if (mode == "production") { 54 | plugins.push(new HtmlInlineScriptPlugin()); 55 | } 56 | 57 | let entry = ["./src/index.ts", "./src/style.css"]; 58 | if (env.test) { 59 | entry.push("./tests/tests.ts"); 60 | } 61 | return { 62 | entry: entry, 63 | output: { 64 | filename: "bundle.js", 65 | path: target, 66 | clean: true, 67 | }, 68 | devtool: devtool, 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.css$/, 73 | use: [ 74 | { 75 | loader: "style-loader", 76 | options: { 77 | insert: "head", // Insert style tag inside of 78 | injectType: "singletonStyleTag", // Wrap all style in just one style tag 79 | }, 80 | }, 81 | "css-loader", 82 | ], 83 | }, 84 | { 85 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 86 | type: "asset/inline", 87 | }, 88 | { 89 | test: /\.ts$/, 90 | use: "ts-loader", 91 | exclude: /node_modules/, 92 | }, 93 | { 94 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 95 | type: "asset/inline", 96 | }, 97 | ], 98 | }, 99 | resolve: { 100 | extensions: [".ts", ".js"], 101 | }, 102 | plugins: plugins, 103 | optimization: { 104 | minimize: minimize, 105 | }, 106 | mode: mode, 107 | devServer: { 108 | contentBase: "./", 109 | }, 110 | }; 111 | }; 112 | -------------------------------------------------------------------------------- /tests/robot_out_stream_tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def resources_dir(tmpdir_factory): 9 | f = __file__ 10 | resources_dir = os.path.join(os.path.dirname(f), "_resources") 11 | assert os.path.exists(resources_dir) 12 | return resources_dir 13 | 14 | 15 | @pytest.fixture(scope="session") 16 | def rcc_loc(tmpdir_factory): 17 | import subprocess 18 | 19 | dirname = tmpdir_factory.mktemp("rcc_dir") 20 | location = os.path.join(str(dirname), "rcc") 21 | if sys.platform == "win32": 22 | location += ".exe" 23 | _download_rcc(location, force=False) 24 | assert os.path.exists(location) 25 | 26 | # Disable tracking for tests 27 | subprocess.check_call([location] + "configure identity --do-not-track".split()) 28 | return location 29 | 30 | 31 | @pytest.fixture(scope="session") 32 | def path_for_tests_robot() -> Path: 33 | f = Path(__file__).parent 34 | robotframework_output_stream_root = f / ".." / ".." 35 | contents = os.listdir(robotframework_output_stream_root) 36 | assert "output-webview" in contents 37 | ret = robotframework_output_stream_root / "output-webview" 38 | assert ret.exists() 39 | ret = ret / "tests" 40 | assert ret.exists() 41 | return ret 42 | 43 | 44 | def _download_rcc(location: str, force: bool = False) -> None: 45 | """ 46 | Downloads rcc to the given location. Note that we don't overwrite it if it 47 | already exists (unless force == True). 48 | 49 | :param location: 50 | The location to store the rcc executable in the filesystem. 51 | :param force: 52 | Whether we should overwrite an existing installation. 53 | """ 54 | 55 | if not os.path.exists(location) or force: 56 | if not os.path.exists(location) or force: 57 | import platform 58 | import urllib.request 59 | 60 | machine = platform.machine() 61 | is_64 = not machine or "64" in machine 62 | 63 | if sys.platform == "win32": 64 | if is_64: 65 | relative_path = "/windows64/rcc.exe" 66 | else: 67 | relative_path = "/windows32/rcc.exe" 68 | 69 | elif sys.platform == "darwin": 70 | relative_path = "/macos64/rcc" 71 | 72 | else: 73 | if is_64: 74 | relative_path = "/linux64/rcc" 75 | else: 76 | relative_path = "/linux32/rcc" 77 | 78 | RCC_VERSION = "v13.5.5" 79 | prefix = f"https://downloads.robocorp.com/rcc/releases/{RCC_VERSION}" 80 | url = prefix + relative_path 81 | 82 | # log.info(f"Downloading rcc from: {url} to: {location}.") 83 | response = urllib.request.urlopen(url) 84 | 85 | # Put it all in memory before writing (i.e. just write it if 86 | # we know we downloaded everything). 87 | data = response.read() 88 | 89 | try: 90 | os.makedirs(os.path.dirname(location)) 91 | except Exception: 92 | pass # Error expected if the parent dir already exists. 93 | 94 | try: 95 | with open(location, "wb") as stream: 96 | stream.write(data) 97 | os.chmod(location, 0x744) 98 | except Exception: 99 | sys.stderr.write( 100 | f"Error writing to: {location}.\nParent dir exists: {os.path.exists(os.path.dirname(location))}\n" 101 | ) 102 | raise 103 | -------------------------------------------------------------------------------- /docs/handling_sensitive_data.md: -------------------------------------------------------------------------------- 1 | ## Handling sensitive data 2 | 3 | By default `Robot Framework Output Stream` will show information for all the `Keyword` calls as well as the parameters passed to keywords. 4 | This is very handy but comes with the drawback that some care must be must be taken in order for sensitive data to be kept out of the logs. 5 | 6 | The most common use cases and APIs are explained below: 7 | 8 | Usernames and passwords 9 | ------------------------ 10 | 11 | For usernames and passwords, the preferred approach is that the provider of the sensitive information 12 | asks for the information and requests `Robot Framework Output Stream` to keep such information out of 13 | the logs. 14 | 15 | The usage for the API is: 16 | 17 | ```python 18 | 19 | password = request_password() 20 | 21 | import robot_out_stream 22 | robot_out_stream.hide_from_output(password) 23 | ``` 24 | 25 | By calling the `hide_from_output` method, any further occurrence of the `password` contents will be 26 | automatically changed to ``. 27 | 28 | Note that any variable in Robot Framework assigned to a variable which has `password` or `passwd` in 29 | its name is also automatically redacted. 30 | 31 | In the example below, the contents of the `${user password}` variable will be automatically added to 32 | the list of strings to be hidden from the output. 33 | 34 | ```robotframework 35 | 36 | *** Keyword *** 37 | Check handling of password 38 | ${user password}= Obtain password 39 | 40 | # The contents of ${user password} will be shown as `` in the log. 41 | Log ${user password} 42 | ``` 43 | 44 | 45 | Sensitive data obtained from APIs 46 | ---------------------------------- 47 | 48 | When handling sensitive data from APIs (such as private user information obtained from an API, as the SSN 49 | or medical data) the preferred API is disabling the logging for variables. 50 | 51 | This can be done either through a `log:ignore-variables` tag in the related keyword 52 | (which may be preferred as the stop/start is managed) or through the 53 | `Stop logging variables`, `Start logging variables` APIs. 54 | 55 | Example using tag: 56 | 57 | ```robotframework 58 | 59 | *** Keyword *** 60 | Handle sensitive info 61 | [Tags] log:ignore-variables 62 | # Obtain and handle sensitive info 63 | 64 | ``` 65 | 66 | Example using API: 67 | 68 | ```robotframework 69 | 70 | *** Settings *** 71 | Library robot_out_stream 72 | 73 | *** Keyword *** 74 | Handle sensitive info 75 | Stop logging variables 76 | TRY 77 | # Obtain and handle sensitive info 78 | 79 | FINALLY 80 | Start logging variables 81 | END 82 | 83 | 84 | ``` 85 | 86 | 87 | If even the keywords called could be used to compromise some information, it's possible 88 | to completely stop the logging with the `log:ignore-keywords` tag or through the 89 | `Stop logging keywords` and `Start logging keywords` APIs. 90 | 91 | Note: this may make debugging a failure harder as keyword calls won't be logged, 92 | albeit you may still call `Log` with a log level of `WARN, FAIL or ERROR` to explicitly log something in this case. 93 | 94 | Example using tag: 95 | 96 | *** Keyword *** 97 | Handle sensitive info 98 | [Tags] log:ignore-keywords 99 | # Obtain and handle sensitive info 100 | 101 | ``` 102 | 103 | Example using API: 104 | 105 | ```robotframework 106 | 107 | *** Settings *** 108 | Library robot_out_stream 109 | 110 | *** Keyword *** 111 | Handle sensitive info 112 | Stop logging keywords 113 | TRY 114 | # Obtain and handle sensitive info 115 | Log This will appear level=WARN 116 | Log This will not appear 117 | 118 | FINALLY 119 | Start logging keywords 120 | END 121 | 122 | 123 | ``` 124 | 125 | 126 | -------------------------------------------------------------------------------- /output-webview/src/sample.ts: -------------------------------------------------------------------------------- 1 | export function getSampleContents() { 2 | return JSON.parse( 3 | '"V 1\\nI \\"sys.platform=win32\\"\\nI \\"python=3.7.6 (default, Jan 8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)]\\"\\nI \\"robot=5.1a3.dev1\\"\\nT 2022-10-19T09:48:34.018\\nM a:\\"Robot1\\"\\nM b:\\"s1\\"\\nM c:\\"C:\\\\\\\\Users\\\\\\\\fabio\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\pytest-of-fabio\\\\\\\\pytest-421\\\\\\\\test_robot_out_stream0\\\\\\\\test_robot_out_stream\\\\\\\\robot1.robot\\"\\nSS a|b|c|0.035\\nM d:\\"Simple Task\\"\\nM e:\\"s1-t1\\"\\nST d|e|16|0.036\\nM f:\\"First keyword\\"\\nM g:\\"\\"\\nM h:\\"KEYWORD\\"\\nSK f|g|h|g|c|17|0.036\\nM i:\\"No Operation\\"\\nM j:\\"BuiltIn\\"\\nM k:\\"Does absolutely nothing.\\"\\nSK i|j|h|k|c|8|0.037\\nM l:\\"PASS\\"\\nEK l|0.037\\nM m:\\"Log\\"\\nM n:\\"Logs the given message with the given level.\\"\\nSK m|j|h|n|c|10|0.037\\nM o:\\"Some warning message\\"\\nKA o\\nM p:\\"level=WARN\\"\\nKA p\\nEK l|0.046\\nM q:\\"Another keyword\\"\\nM r:\\"another\\"\\nSK q|r|h|g|c|11|0.047\\nM s:\\"C:\\\\\\\\Users\\\\\\\\fabio\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\pytest-of-fabio\\\\\\\\pytest-421\\\\\\\\test_robot_out_stream0\\\\\\\\test_robot_out_stream\\\\\\\\another.robot\\"\\nSK i|j|h|k|s|3|0.047\\nEK l|0.047\\nEK l|0.047\\nM t:\\"Another in sub keyword\\"\\nM u:\\"another_sub\\"\\nSK t|u|h|g|c|12|0.047\\nM v:\\"C:\\\\\\\\Users\\\\\\\\fabio\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\pytest-of-fabio\\\\\\\\pytest-421\\\\\\\\test_robot_out_stream0\\\\\\\\test_robot_out_stream\\\\\\\\sub\\\\\\\\another_sub.robot\\"\\nSK i|j|h|k|v|6|0.048\\nEK l|0.048\\nSK m|j|h|n|v|7|0.048\\nM w:\\"Some error message\\"\\nKA w\\nM x:\\"level=ERROR\\"\\nKA x\\nEK l|0.049\\nEK l|0.049\\nEK l|0.049\\nSK m|j|h|n|c|18|0.049\\nM y:\\"Some \\"\\nKA y\\nEK l|0.049\\nM z:\\"Create Dictionary\\"\\nM A:\\"Creates and returns a dictionary based on the given ``items``.\\"\\nSK z|j|h|A|c|19|0.049\\nM B:\\"a=1\\"\\nKA B\\nM C:\\"b=1\\"\\nKA C\\nEK l|0.05\\nSK m|j|h|n|c|20|0.05\\nM D:\\"${dct}\\"\\nKA D\\nEK l|0.051\\nET l|g|0.051\\nM E:\\"Check 1\\"\\nM F:\\"s1-t2\\"\\nST E|F|22|0.051\\nSK f|g|h|g|c|23|0.051\\nSK i|j|h|k|c|8|0.052\\nEK l|0.052\\nSK m|j|h|n|c|10|0.052\\nKA o\\nKA p\\nEK l|0.052\\nSK q|r|h|g|c|11|0.053\\nSK i|j|h|k|s|3|0.053\\nEK l|0.053\\nEK l|0.053\\nSK t|u|h|g|c|12|0.053\\nSK i|j|h|k|v|6|0.054\\nEK l|0.054\\nSK m|j|h|n|v|7|0.054\\nKA w\\nKA x\\nEK l|0.054\\nEK l|0.055\\nEK l|0.055\\nM G:\\"${counter} IN RANGE [ 0 | 3 ]\\"\\nM H:\\"FOR\\"\\nSK G|g|H|g|c|25|0.055\\nM I:\\"${counter} = 0\\"\\nM J:\\"ITERATION\\"\\nSK I|g|J|g|c|25|0.055\\nM K:\\"${counter} == 2\\"\\nM L:\\"IF\\"\\nSK K|g|L|g|c|26|0.055\\nM M:\\"Fail\\"\\nM N:\\"Fails the test with the given message and optionally alters its tags.\\"\\nSK M|j|h|N|c|27|0.056\\nM O:\\"Failed execution for some reason...\\"\\nKA O\\nM P:\\"NOT RUN\\"\\nEK P|0.056\\nEK P|0.056\\nSK m|j|h|n|c|29|0.056\\nM Q:\\"${counter}\\"\\nKA Q\\nEK l|0.056\\nEK l|0.057\\nM R:\\"${counter} = 1\\"\\nSK R|g|J|g|c|25|0.057\\nSK K|g|L|g|c|26|0.057\\nSK M|j|h|N|c|27|0.057\\nKA O\\nEK P|0.057\\nEK P|0.057\\nSK m|j|h|n|c|29|0.058\\nKA Q\\nEK l|0.058\\nEK l|0.058\\nM S:\\"${counter} = 2\\"\\nSK S|g|J|g|c|25|0.058\\nSK K|g|L|g|c|26|0.058\\nSK M|j|h|N|c|27|0.059\\nKA O\\nM T:\\"FAIL\\"\\nEK T|0.059\\nEK T|0.059\\nSK m|j|h|n|c|29|0.059\\nKA Q\\nEK P|0.059\\nEK T|0.06\\nEK T|0.06\\nET T|O|0.06\\nM U:\\"Check 2\\"\\nM V:\\"s1-t3\\"\\nST U|V|32|0.06\\nM W:\\"Set Variable\\"\\nM X:\\"Returns the given values which can then be assigned to a variables.\\"\\nSK W|j|h|X|c|33|0.061\\nM Y:\\"3\\"\\nKA Y\\nEK l|0.061\\nM Z:\\"${counter} <= 2\\"\\nM 0:\\"WHILE\\"\\nSK Z|g|0|g|c|34|0.061\\nSK g|g|J|g|c|34|0.062\\nM 1:\\"Evaluate\\"\\nM 2:\\"Evaluates the given expression in Python and returns the result.\\"\\nSK 1|j|h|2|c|35|0.062\\nM 4:\\"$counter-1\\"\\nKA 4\\nEK P|0.062\\nSK m|j|h|n|c|36|0.062\\nM 5:\\"Current counter: ${counter}\\"\\nKA 5\\nKA p\\nEK P|0.062\\nEK P|0.062\\nEK P|0.063\\nET l|g|0.063\\nM 6:\\"Check 3\\"\\nM 7:\\"s1-t4\\"\\nST 6|7|39|0.064\\nM 8:\\"TRY\\"\\nSK g|g|8|g|c|40|0.064\\nSK i|j|h|k|c|41|0.064\\nEK l|0.064\\nEK l|0.064\\nM 9:\\"message\\"\\nM aa:\\"EXCEPT\\"\\nSK 9|g|aa|g|c|42|0.064\\nSK i|j|h|k|c|43|0.065\\nEK P|0.065\\nEK P|0.065\\nM ab:\\"FINALLY\\"\\nSK g|g|ab|g|c|44|0.065\\nSK i|j|h|k|c|45|0.065\\nEK l|0.065\\nEK l|0.065\\nET l|g|0.066\\nES T|0.067\\n"' 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /src/robot_out_stream/_decoder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | 5 | def _decode_oid(decoder, oid): 6 | return decoder.memo[oid] 7 | 8 | 9 | def _decode_float(decoder, msg): 10 | return float(msg) 11 | 12 | 13 | def _decode_int(decoder, msg): 14 | return int(msg) 15 | 16 | 17 | def _decode_str(decoder, msg): 18 | return msg 19 | 20 | 21 | def _decode(message_definition, level_diff=0): 22 | names = [] 23 | name_to_decode = {} 24 | for s in message_definition.split(","): 25 | s = s.strip() 26 | i = s.find(":") 27 | decode = "oid" 28 | if i != -1: 29 | s, decode = s.split(":", 1) 30 | names.append(s) 31 | if decode == "oid": 32 | name_to_decode[s] = _decode_oid 33 | 34 | elif decode == "int": 35 | name_to_decode[s] = _decode_int 36 | 37 | elif decode == "float": 38 | name_to_decode[s] = _decode_float 39 | 40 | elif decode == "str": 41 | name_to_decode[s] = _decode_str 42 | 43 | else: 44 | raise RuntimeError(f"Unexpected: {decode}") 45 | 46 | def dec_impl(decoder, message): 47 | decoder.level += level_diff 48 | splitted = message.split("|", len(names) - 1) 49 | ret = {} 50 | for i, s in enumerate(splitted): 51 | name = names[i] 52 | try: 53 | ret[name] = name_to_decode[name](decoder, s) 54 | except: 55 | ret[name] = None 56 | return ret 57 | 58 | return dec_impl 59 | 60 | 61 | def decode_time(decoder, time): 62 | decoder.initial_time = datetime.datetime.fromisoformat(time) 63 | return {"initial_time": time} 64 | 65 | 66 | def decode_memo(decoder, message): 67 | memo_id, memo_value = message.split(":", 1) 68 | memo_value = json.loads(memo_value) 69 | decoder.memo[memo_id] = memo_value 70 | return None 71 | 72 | 73 | _MESSAGE_TYPE_INFO = { 74 | "V": lambda _decoder, message: {"version": message}, 75 | "I": lambda _decoder, message: {"info": json.loads(message)}, 76 | "ID": _decode("part:int, id:str"), 77 | "T": decode_time, 78 | "M": decode_memo, 79 | "L": _decode("level:str, message:oid, time_delta_in_seconds:float"), 80 | "LH": _decode("level:str, message:oid, time_delta_in_seconds:float"), 81 | "SS": _decode( 82 | "name:oid, suite_id:oid, suite_source:oid, time_delta_in_seconds:float", 83 | level_diff=+1, 84 | ), 85 | "ES": _decode("status:oid, time_delta_in_seconds:float", level_diff=-1), 86 | "ST": _decode( 87 | "name:oid, suite_id:oid, lineno:int, time_delta_in_seconds:float", level_diff=+1 88 | ), 89 | "ET": _decode( 90 | "status:oid, message:oid, time_delta_in_seconds:float", level_diff=-1 91 | ), 92 | "SK": _decode( 93 | "name:oid, libname:oid, keyword_type:oid, doc:oid, source:oid, lineno:int, time_delta_in_seconds:float", 94 | level_diff=+1, 95 | ), 96 | "EK": _decode("status:oid, time_delta_in_seconds:float", level_diff=-1), 97 | "KA": _decode("argument:oid"), 98 | "AS": _decode("assign:oid"), 99 | "TG": _decode("tag:oid"), 100 | "S": _decode("start_time_delta:float"), 101 | } 102 | 103 | _MESSAGE_TYPE_INFO["RS"] = _MESSAGE_TYPE_INFO["SS"] 104 | _MESSAGE_TYPE_INFO["RT"] = _MESSAGE_TYPE_INFO["ST"] 105 | _MESSAGE_TYPE_INFO["RK"] = _MESSAGE_TYPE_INFO["SK"] 106 | 107 | 108 | class Decoder: 109 | def __init__(self): 110 | self.memo = {} 111 | self.initial_time = None 112 | self.level = 0 113 | 114 | @property 115 | def ident(self): 116 | return " " * self.level 117 | 118 | def decode_message_type(self, message_type, message): 119 | handler = _MESSAGE_TYPE_INFO[message_type] 120 | ret = {"message_type": message_type} 121 | try: 122 | r = handler(self, message) 123 | if not r: 124 | if message_type == "M": 125 | return None 126 | raise RuntimeError( 127 | f"No return when decoding: {message_type} - {message}" 128 | ) 129 | if not isinstance(r, dict): 130 | ret[ 131 | "error" 132 | ] = f"Expected dict return when decoding: {message_type} - {message}. Found: {ret}" 133 | 134 | ret.update(r) 135 | except Exception as e: 136 | ret["error"] = f"Error decoding: {message_type}: {e}" 137 | return ret 138 | 139 | 140 | def iter_decoded_log_format(stream): 141 | decoder = Decoder() 142 | for line in stream.readlines(): 143 | line = line.strip() 144 | if line: 145 | message_type, message = line.split(" ", 1) 146 | decoded = decoder.decode_message_type(message_type, message) 147 | if decoded: 148 | yield decoded 149 | -------------------------------------------------------------------------------- /output-webview/src/decoder.ts: -------------------------------------------------------------------------------- 1 | // Based on the python decoder example. 2 | // Used: https://extendsclass.com/python-to-javascript.html 3 | 4 | import parseISO from "date-fns/parseISO"; 5 | 6 | export interface IMessage { 7 | readonly message_type: string; 8 | readonly decoded: any; 9 | } 10 | 11 | function version_decode(decoder, message) { 12 | return { message }; 13 | } 14 | 15 | function simple_decode(decoder, message) { 16 | return JSON.parse(message); 17 | } 18 | 19 | function decode_time(decoder, time) { 20 | decoder.initial_time = parseISO(time); 21 | return { time }; 22 | } 23 | 24 | function decode_memo(decoder, message) { 25 | var memo_id, memo_value; 26 | const split = splitInChar(message, ":"); 27 | if (split) { 28 | [memo_id, memo_value] = split; 29 | try { 30 | memo_value = JSON.parse(memo_value); 31 | } catch (error) { 32 | console.log("Error parsing json: " + memo_value); 33 | console.log(error); 34 | return null; 35 | } 36 | decoder.memo[memo_id] = memo_value; 37 | } 38 | 39 | return null; 40 | } 41 | 42 | function _decodeOid(decoder, oid) { 43 | const ret = decoder.memo[oid]; 44 | if (ret === undefined) { 45 | return ``; 46 | } 47 | return ret; 48 | } 49 | function _decodeFloat(decoder, msg) { 50 | return parseFloat(msg); 51 | } 52 | function _decodeInt(decoder, msg) { 53 | return parseInt(msg); 54 | } 55 | 56 | function _decodeStr(decoder, msg) { 57 | return msg; 58 | } 59 | 60 | function _decode(message_definiton) { 61 | const names = []; 62 | const nameToDecode = new Map(); 63 | for (let s of message_definiton.split(",")) { 64 | s = s.trim(); 65 | const i = s.indexOf(":"); 66 | let decode = "oid"; 67 | if (i != -1) { 68 | [s, decode] = s.split(":", 2); 69 | } 70 | names.push(s); 71 | if (decode === "oid") { 72 | nameToDecode.set(s, _decodeOid); 73 | } else if (decode === "int") { 74 | nameToDecode.set(s, _decodeInt); 75 | } else if (decode === "float") { 76 | nameToDecode.set(s, _decodeFloat); 77 | } else if (decode === "str") { 78 | nameToDecode.set(s, _decodeStr); 79 | } else { 80 | throw new Error("Unexpected: " + decode); 81 | } 82 | } 83 | 84 | function _decImpl(decoder, message) { 85 | const splitted = message.split("|", names.length); 86 | const ret = {}; 87 | for (let index = 0; index < splitted.length; index++) { 88 | const s = splitted[index]; 89 | const name = names[index]; 90 | ret[name] = nameToDecode.get(name)(decoder, s); 91 | } 92 | // console.log("decoded", ret); 93 | return ret; 94 | } 95 | return _decImpl; 96 | } 97 | 98 | const start_suite = _decode("name:oid, suite_id:oid, suite_source:oid, time_delta_in_seconds:float"); 99 | 100 | const end_suite = _decode("status:oid, time_delta_in_seconds:float"); 101 | 102 | const start_task_or_test = _decode("name:oid, suite_id:oid, lineno:int, time_delta_in_seconds:float"); 103 | 104 | const end_task_or_test = _decode("status:oid, message:oid, time_delta_in_seconds:float"); 105 | 106 | const start_keyword = _decode( 107 | "name:oid, libname:oid, keyword_type:oid, doc:oid, source:oid, lineno:int, time_delta_in_seconds:float" 108 | ); 109 | 110 | const end_keyword = _decode("status:oid, time_delta_in_seconds:float"); 111 | 112 | const decode_log = _decode("level:str, message:oid, time_delta_in_seconds:float"); 113 | 114 | const id_decode = _decode("part:int, id:str"); 115 | 116 | const _MESSAGE_TYPE_INFO = { 117 | "V": version_decode, 118 | "ID": id_decode, 119 | "I": simple_decode, 120 | "T": decode_time, 121 | "M": decode_memo, 122 | "SS": start_suite, 123 | "RS": start_suite, 124 | "ES": end_suite, 125 | "ST": start_task_or_test, 126 | "RT": start_task_or_test, 127 | "ET": end_task_or_test, 128 | "SK": start_keyword, 129 | "RK": start_keyword, 130 | "EK": end_keyword, 131 | "KA": _decode("argument:oid"), 132 | "L": decode_log, 133 | "LH": decode_log, 134 | "AS": _decode("assign:oid"), 135 | "TG": _decode("tag:oid"), 136 | "S": _decode("start_time_delta:float"), 137 | }; 138 | 139 | export class Decoder { 140 | memo; 141 | initial_time; 142 | level; 143 | ident; 144 | 145 | constructor() { 146 | this.memo = {}; 147 | this.initial_time = null; 148 | this.level = 0; 149 | this.ident = ""; 150 | } 151 | 152 | decode_message_type(message_type, message) { 153 | var handler; 154 | handler = _MESSAGE_TYPE_INFO[message_type]; 155 | return handler(this, message); 156 | } 157 | } 158 | 159 | function splitInChar(line: string, char: string) { 160 | const i = line.indexOf(char); 161 | if (i > 0) { 162 | const message_type = line.substring(0, i); 163 | const message = line.substring(i + 1); 164 | return [message_type, message]; 165 | } 166 | return undefined; 167 | } 168 | 169 | export function* iter_decoded_log_format(stream: string, decoder: Decoder) { 170 | var decoded, message, message_type; 171 | 172 | for (let line of stream.split(/\r?\n/)) { 173 | line = line.trim(); 174 | 175 | if (line) { 176 | const split = splitInChar(line, " "); 177 | if (split) { 178 | [message_type, message] = split; 179 | decoded = decoder.decode_message_type(message_type, message); 180 | 181 | if (decoded) { 182 | const m: IMessage = { "message_type": message_type, "decoded": decoded }; 183 | yield m; 184 | } 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /output-webview/src/vscodeComm.ts: -------------------------------------------------------------------------------- 1 | import { IFilterLevel, IState } from "./protocols"; 2 | 3 | interface IVSCode { 4 | postMessage(message: any): void; 5 | getState(): any; 6 | setState(state: any); 7 | } 8 | 9 | export let vscode: IVSCode; // Loaded in index.htoml 10 | function setVSCodeAPI(api: IVSCode) { 11 | vscode = api; 12 | } 13 | window["setVSCodeAPI"] = setVSCodeAPI; 14 | 15 | // Note how request/response/event follows the same patterns from the 16 | // DAP (debug adapter protocol). 17 | export interface IRequestMessage { 18 | type: "request"; 19 | seq: number; 20 | command: string; 21 | } 22 | 23 | export interface IResponseMessage { 24 | type: "response"; 25 | seq: number; 26 | command: string; 27 | request_seq: number; 28 | body?: any; 29 | } 30 | 31 | export interface IEventMessage { 32 | type: "event"; 33 | seq: number; 34 | event: string; 35 | } 36 | 37 | export interface ISetContentsRequest { 38 | type: "request"; 39 | command: "setContents"; 40 | initialContents: string; 41 | runId: string; 42 | allRunIdsToLabel: object; 43 | } 44 | 45 | export interface IAppendContentsRequest { 46 | type: "request"; 47 | command: "appendContents"; 48 | appendContents: string; 49 | runId: string; 50 | } 51 | 52 | export interface IUpdateLabelRequest { 53 | type: "request"; 54 | command: "updateLabel"; 55 | runId: string; 56 | label: string; 57 | } 58 | 59 | let msgIdToSeq = {}; 60 | 61 | export function sendRequestToClient(message: IRequestMessage): Promise { 62 | let vscodeRef = undefined; 63 | try { 64 | vscodeRef = vscode; 65 | } catch (err) { 66 | // ignore 67 | } 68 | 69 | if (vscodeRef) { 70 | let promise = new Promise((resolve, reject) => { 71 | msgIdToSeq[message.seq] = resolve; 72 | }); 73 | vscodeRef.postMessage(message); 74 | return promise; 75 | } else { 76 | // Unable to send to VSCode because we're not really connected 77 | // (case when html is opened directly and not through VSCode). 78 | return new Promise((resolve, reject) => { 79 | let response: IResponseMessage = { 80 | type: "response", 81 | seq: nextMessageSeq(), 82 | command: message.command, 83 | request_seq: message.seq, 84 | body: undefined, 85 | }; 86 | resolve(response); 87 | }); 88 | } 89 | } 90 | 91 | export function sendEventToClient(message: IEventMessage): void { 92 | // console.log("send event", message); 93 | let vscodeRef = undefined; 94 | try { 95 | vscodeRef = vscode; 96 | } catch (err) { 97 | // ignore 98 | } 99 | 100 | if (vscodeRef) { 101 | vscodeRef.postMessage(message); 102 | } 103 | } 104 | 105 | export let eventToHandler = { 106 | "output": undefined, 107 | }; 108 | 109 | export let requestToHandler = { 110 | "setContents": undefined, 111 | "appendContents": undefined, 112 | "updateLabel": undefined, 113 | }; 114 | 115 | // i.e.: Receive message from client 116 | window.addEventListener("message", (event) => { 117 | let msg = event.data; 118 | if (msg) { 119 | switch (msg.type) { 120 | case "response": 121 | // Response to something we posted. 122 | let responseMsg: IResponseMessage = msg; 123 | let resolvePromise = msgIdToSeq[responseMsg.request_seq]; 124 | if (resolvePromise) { 125 | delete msgIdToSeq[responseMsg.request_seq]; 126 | resolvePromise(responseMsg); 127 | } 128 | break; 129 | case "event": 130 | // Process some event 131 | let handler = eventToHandler[msg.event]; 132 | if (handler) { 133 | handler(msg); 134 | } else { 135 | console.log("Unhandled event: ", msg); 136 | } 137 | break; 138 | case "request": 139 | // Process some request 140 | let requestHandler = requestToHandler[msg.command]; 141 | if (requestHandler) { 142 | requestHandler(msg); 143 | } else { 144 | console.log("Unhandled request: ", msg); 145 | } 146 | break; 147 | } 148 | } 149 | }); 150 | 151 | let _lastMessageId: number = 0; 152 | export function nextMessageSeq(): number { 153 | _lastMessageId += 1; 154 | return _lastMessageId; 155 | } 156 | 157 | let _globalState: IState = { filterLevel: "PASS", runIdToTreeState: {}, runIdLRU: [] }; 158 | 159 | export function getState(): IState { 160 | let vscodeRef = undefined; 161 | try { 162 | vscodeRef = vscode; 163 | } catch (err) {} 164 | 165 | if (vscodeRef) { 166 | let ret: IState = vscodeRef.getState(); 167 | if (!ret) { 168 | // Initial state. 169 | ret = _globalState; 170 | } 171 | if (ret.filterLevel === undefined) { 172 | ret.filterLevel = "PASS"; 173 | } 174 | if (ret.runIdToTreeState === undefined) { 175 | ret.runIdToTreeState = {}; 176 | } 177 | if (!ret.runIdLRU === undefined) { 178 | ret.runIdLRU = []; 179 | } 180 | // console.log("getState", JSON.stringify(ret)); 181 | return ret; 182 | } 183 | // console.log("getState - empty"); 184 | return _globalState; 185 | } 186 | 187 | export function setState(state: IState) { 188 | // console.log("setState", JSON.stringify(state)); 189 | let vscodeRef = undefined; 190 | try { 191 | vscodeRef = vscode; 192 | } catch (err) {} 193 | 194 | if (vscodeRef) { 195 | vscodeRef.setState(state); 196 | } else { 197 | _globalState = state; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /output-webview/tests/test_output_view.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Tests for the Output View. 3 | 4 | Library RPA.Browser.Playwright 5 | Library String 6 | Library json 7 | Library RPA.FileSystem 8 | Library Collections 9 | Library ./uris.py 10 | 11 | Test Teardown Close Browser 12 | 13 | 14 | *** Variables *** 15 | # ${HEADLESS} False 16 | ${HEADLESS} True 17 | 18 | 19 | *** Test Cases *** 20 | Test Scenario 1 simple 21 | [Documentation] 22 | ... A simple scenario where the output view is opened with a 23 | ... single test which passed without any keywords. 24 | Open Output View For Tests 25 | Setup Scenario ${CURDIR}/_resources/case1.rfstream 26 | Check Labels 1 27 | Check Tree Items Text Robot1.Simple Task 28 | 29 | Test Scenario 2 restart 30 | [Documentation] 31 | ... A simple scenario where the output view is opened with a 32 | ... single test which passed without any keywords (with ignored restart). 33 | Open Output View For Tests 34 | Setup Scenario ${CURDIR}/_resources/case2.rfstream 35 | Check Labels 1 36 | Check Tree Items Text Robot1.Simple Task 37 | 38 | Test Scenario 3 only restart 39 | [Documentation] 40 | ... A simple scenario where the output view is opened with a 41 | ... single test which passed without any keywords (just with restart). 42 | Open Output View For Tests 43 | Setup Scenario ${CURDIR}/_resources/case3.rfstream 44 | Check Labels 1 45 | Check Tree Items Text Robot1.Simple Task 46 | 47 | Test Scenario 4 screenshot 48 | [Documentation] 49 | ... A scenario with a screenshot. 50 | Open Output View For Tests 51 | Setup Scenario ${CURDIR}/_resources/case4.rfstream 52 | Check Image 53 | Check Tree Items Text Scenario Generator.Screenshot test 54 | ... KEYWORD - RPA.Desktop.Take Screenshot | output/test_screenshot.png | embed\=True 55 | ... Saved screenshot as 'output\\test_screenshot.png' 56 | ... ${EMPTY} 57 | 58 | Test Scenario 5 filtering NOT RUN 59 | [Documentation] 60 | ... A scenario with many elements in a FOR (some not run). 61 | Open Output View For Tests 62 | Setup Scenario ${CURDIR}/_resources/case5.rfstream 63 | # Default filtering is PASS. 64 | Check Labels From Pass Onwards 65 | 66 | # Change filtering to NOT RUN. 67 | RPA.Browser.Playwright.Select Options By \#filterLevel value NOT RUN 68 | Check Labels From Not Run Onwards 69 | 70 | Test Scenario 5 hide too many loops 71 | [Documentation] 72 | ... A scenario with many elements in a FOR (some not run). 73 | Open Output View For Tests 74 | Setup Scenario ${CURDIR}/_resources/case5.rfstream 75 | ${text_items}= Get Text From Labels 76 | BuiltIn.Should Contain X Times ${text_items} HIDDEN 19 77 | 78 | 79 | *** Keywords *** 80 | Check Labels From Not Run Onwards 81 | ${text_items}= Get Text From Labels 82 | ${text_items}= Remove Duplicates ${text_items} 83 | Should Contain ${text_items} NOT RUN 84 | ${expected}= Evaluate ['PASS', 'LOG INFO', 'NOT RUN', 'LOG ERROR', 'HIDDEN', '...'] 85 | Should Be Equal As Strings ${expected} ${text_items} 86 | 87 | Check Labels From Pass Onwards 88 | ${text_items}= Get Text From Labels 89 | ${text_items}= Remove Duplicates ${text_items} 90 | Should Not Contain ${text_items} NOT RUN 91 | ${expected}= Evaluate ['PASS', 'LOG INFO', 'LOG ERROR', 'HIDDEN', '...'] 92 | Should Be Equal As Strings ${expected} ${text_items} 93 | 94 | Check Integers Equal 95 | [Arguments] ${a} ${b} 96 | ${a}= Convert To Integer ${a} 97 | ${b}= Convert To Integer ${b} 98 | Builtin.Should Be Equal ${a} ${b} 99 | 100 | Open Output View For Tests 101 | ${curdir_proper_slashes}= Replace String ${CURDIR} \\ / 102 | ${filepath}= Set Variable ${curdir_proper_slashes}/../dist-test/index.html 103 | ${exists}= RPA.FileSystem.Does File Exist ${filepath} 104 | Should Be True ${exists} File "${filepath}"" does not exist (distribution does not seem to be built). 105 | 106 | ${fileuri}= uris.From Fs Path ${filepath} 107 | RPA.Browser.Playwright.Set Browser Timeout 3 108 | Log To Console fileuri=${fileuri} 109 | Open Browser url=${fileuri} headless=${HEADLESS} 110 | 111 | Setup Scenario 112 | [Arguments] ${filename} 113 | ${contents}= RPA.FileSystem.Read File ${filename} 114 | ${contents_as_json}= json.Dumps ${contents} 115 | 116 | Evaluate JavaScript ${None} 117 | ... ()=>{ 118 | ... window['setupScenario'](${contents_as_json}); 119 | ... } 120 | 121 | Get Text From Elements 122 | [Arguments] ${locator} 123 | ${elements}= RPA.Browser.Playwright.Get Elements ${locator} 124 | ${lst}= Builtin.Create List 125 | FOR ${element} IN @{elements} 126 | ${txt}= RPA.Browser.Playwright.Get Text ${element} 127 | Append To List ${lst} ${txt} 128 | END 129 | RETURN ${lst} 130 | 131 | Get Text From Tree Items 132 | ${txt}= Get Text From Elements .span_link 133 | RETURN ${txt} 134 | 135 | Get Text From Labels 136 | ${txt}= Get Text From Elements .label 137 | RETURN ${txt} 138 | 139 | Check Labels 140 | [Arguments] ${expected_number_of_labels} 141 | ${element_count}= RPA.Browser.Playwright.Get Element Count .label 142 | Check Integers Equal ${expected_number_of_labels} ${element_count} 143 | 144 | Check Tree Items Text 145 | [Arguments] @{expected_text_items} 146 | ${text_items}= Get Text From Tree Items 147 | Should Be Equal ${text_items} ${expected_text_items} 148 | 149 | Check Image 150 | ${element_count}= RPA.Browser.Playwright.Get Element Count img 151 | Check Integers Equal 1 ${element_count} 152 | -------------------------------------------------------------------------------- /output-webview/tests/uris.py: -------------------------------------------------------------------------------- 1 | # Original work Copyright 2017 Palantir Technologies, Inc. (MIT) 2 | # Original work Copyright 2020 Open Law Library. (Apache 2) 3 | # See ThirdPartyNotices.txt in the project root for license information. 4 | # All modifications Copyright (c) Robocorp Technologies Inc. 5 | # All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License") 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http: // www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """A collection of URI utilities with logic built on the VSCode URI library. 20 | 21 | https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts 22 | """ 23 | 24 | import re 25 | import sys 26 | from functools import lru_cache 27 | 28 | from urllib.parse import ( 29 | urlparse as parse_urlparse, # noqa 30 | urlunparse as parse_urlunparse, # noqa 31 | quote as parse_quote, # noqa 32 | unquote as parse_unquote, # noqa 33 | ) 34 | 35 | IS_WIN = sys.platform == "win32" 36 | 37 | RE_DRIVE_LETTER_PATH = re.compile(r"^\/[a-zA-Z]:") 38 | 39 | 40 | if sys.platform == "win32": 41 | 42 | def normalize_drive(filename): 43 | if len(filename) > 1 and filename[1] == ":" and not filename[0].islower(): 44 | return filename[0].lower() + filename[1:] 45 | return filename 46 | 47 | 48 | else: 49 | 50 | def normalize_drive(filename): 51 | return filename 52 | 53 | 54 | def _normalize_win_path(path): 55 | netloc = "" 56 | 57 | # normalize to fwd-slashes on windows, 58 | # on other systems bwd-slashes are valid 59 | # filename character, eg /f\oo/ba\r.txt 60 | if IS_WIN: 61 | path = path.replace("\\", "/") 62 | 63 | # check for authority as used in UNC shares 64 | # or use the path as given 65 | if path[:2] == "//": 66 | idx = path.index("/", 2) 67 | if idx == -1: 68 | netloc = path[2:] 69 | else: 70 | netloc = path[2:idx] 71 | path = path[idx:] 72 | 73 | # Ensure that path starts with a slash 74 | # or that it is at least a slash 75 | if not path.startswith("/"): 76 | path = "/" + path 77 | 78 | # Normalize drive paths to lower case 79 | if RE_DRIVE_LETTER_PATH.match(path): 80 | path = path[0] + path[1].lower() + path[2:] 81 | 82 | return path, netloc 83 | 84 | 85 | @lru_cache(500) 86 | def from_fs_path(path: str) -> str: 87 | """Returns a URI for the given filesystem path.""" 88 | scheme = "file" 89 | params, query, fragment = "", "", "" 90 | path, netloc = _normalize_win_path(path) 91 | return urlunparse((scheme, netloc, path, params, query, fragment)) 92 | 93 | 94 | @lru_cache(500) 95 | def normalize_uri(uri: str) -> str: 96 | if uri_scheme(uri) == "file": 97 | return from_fs_path(to_fs_path(uri)) 98 | return uri 99 | 100 | 101 | @lru_cache(500) 102 | def to_fs_path(uri: str) -> str: 103 | """Returns the filesystem path of the given URI. 104 | 105 | Will handle UNC paths and normalize windows drive letters to lower-case. 106 | Also uses the platform specific path separator. Will *not* validate the 107 | path for invalid characters and semantics. 108 | Will *not* look at the scheme of this URI. 109 | """ 110 | # scheme://netloc/path;parameters?query#fragment 111 | scheme, netloc, path, _params, _query, _fragment = urlparse(uri) 112 | 113 | if netloc and path and scheme == "file": 114 | # unc path: file://shares/c$/far/boo 115 | value = "//{}{}".format(netloc, path) 116 | 117 | elif RE_DRIVE_LETTER_PATH.match(path): 118 | # windows drive letter: file:///C:/far/boo 119 | value = path[1].lower() + path[2:] 120 | 121 | else: 122 | # Other path 123 | value = path 124 | 125 | if IS_WIN: 126 | value = value.replace("/", "\\") 127 | value = normalize_drive(value) 128 | 129 | return value 130 | 131 | 132 | def uri_scheme(uri): 133 | try: 134 | return urlparse(uri)[0] 135 | except (TypeError, IndexError): 136 | return None 137 | 138 | 139 | def uri_with( 140 | uri, scheme=None, netloc=None, path=None, params=None, query=None, fragment=None 141 | ): 142 | """Return a URI with the given part(s) replaced. 143 | 144 | Parts are decoded / encoded. 145 | """ 146 | old_scheme, old_netloc, old_path, old_params, old_query, old_fragment = urlparse( 147 | uri 148 | ) 149 | 150 | path, _netloc = _normalize_win_path(path) 151 | return urlunparse( 152 | ( 153 | scheme or old_scheme, 154 | netloc or old_netloc, 155 | path or old_path, 156 | params or old_params, 157 | query or old_query, 158 | fragment or old_fragment, 159 | ) 160 | ) 161 | 162 | 163 | def urlparse(uri): 164 | """Parse and decode the parts of a URI.""" 165 | scheme, netloc, path, params, query, fragment = parse_urlparse(uri) 166 | return ( 167 | parse_unquote(scheme), 168 | parse_unquote(netloc), 169 | parse_unquote(path), 170 | parse_unquote(params), 171 | parse_unquote(query), 172 | parse_unquote(fragment), 173 | ) 174 | 175 | 176 | def urlunparse(parts): 177 | """Unparse and encode parts of a URI.""" 178 | scheme, netloc, path, params, query, fragment = parts 179 | 180 | # Avoid encoding the windows drive letter colon 181 | if RE_DRIVE_LETTER_PATH.match(path): 182 | quoted_path = path[:3] + parse_quote(path[3:]) 183 | else: 184 | quoted_path = parse_quote(path) 185 | 186 | return parse_urlunparse( 187 | ( 188 | parse_quote(scheme), 189 | parse_quote(netloc), 190 | quoted_path, 191 | parse_quote(params), 192 | parse_quote(query), 193 | parse_quote(fragment), 194 | ) 195 | ) 196 | -------------------------------------------------------------------------------- /docs/format.md: -------------------------------------------------------------------------------- 1 | # The `.rfstream` format 2 | 3 | ## Requirements 4 | 5 | The requirements for the generated log files are the following: 6 | 7 | 1. Compact log: 8 | 9 | The files generated should be as compact as possible. Reading the file 10 | may require a separate application (although the idea is still trying 11 | to keep to ASCII instead of a binary format). 12 | 13 | 2. Log streaming: 14 | 15 | The log format should be suitable for streaming (so, it's possible to 16 | interpret the log while it's being written or up to the point a 17 | Python VM crash happened). 18 | 19 | 3. Information: 20 | 21 | While the format of the log should be as compact as possible, it should 22 | be able to provide the needed information to debug an issue, so, 23 | it must track almost all information currently available in the Robot 24 | output.xml. 25 | 26 | 4. Log file rotation: 27 | 28 | If while being written a log becomes too big the log contents should be 29 | rotated to a separate file and it should be possible to specify a maximum 30 | size for the log (even if old information in the log is discarded in this 31 | case). 32 | 33 | 34 | ## Outputs 35 | 36 | The basic log can actually be split into multiple files. 37 | Such files are splitted in the following files (the idea is that it can be split when it becomes too big). 38 | 39 | - `output.rfstream` 40 | - `output_2.rfstream` 41 | - `output_3.rfstream` 42 | - ... 43 | 44 | The file should be always written and flushed at each log entry and it should be consistent even if the process crashes in the meanwhile (meaning that all entries written are valid up to the point of the crash). 45 | 46 | 47 | ## "Basic log" spec 48 | 49 | To keep the format compact, strings will be referenced by an id in the output and the output message types will be predetermined and referenced in the same way. 50 | 51 | Times are referenced by the delta from the start. 52 | 53 | Also, each message should use a single line in the log output where the prefix is the message type and the arguments is either a message with ids/numbers separated by `|` or json-encoded strings. 54 | 55 | Note that each output log file (even if splitted after the main one) should be readable in a completely independent way, so, the starting scope should be replicated as well as the needed names to memorize. 56 | 57 | Basic message types are: 58 | 59 | ### V: Version(name) 60 | 61 | Identifies the version of the log being used 62 | 63 | Example: 64 | 65 | `V 1` - Identifies version 1 of the log 66 | 67 | ### ID: Identifier and part for this run. 68 | 69 | Provides a unique id for the run (which should be the same even if the 70 | file is split among multiple files) as well as the identification of 71 | which is the current part. 72 | 73 | Example: 74 | 75 | `ID: 1|36ac1f85-6d32-45b0-8ebf-3bbf8d7482f2` 1st part with identifier 36ac1f85-6d32-45b0-8ebf-3bbf8d7482f2. 76 | `ID: 2|36ac1f85-6d32-45b0-8ebf-3bbf8d7482f2` 2nd part with identifier 36ac1f85-6d32-45b0-8ebf-3bbf8d7482f2. 77 | 78 | ### I: Info(info_as_json_string) 79 | 80 | Example: 81 | 82 | `I "python=3.7"` 83 | `I "RF=5.7.0"` 84 | 85 | ### M: Memorize name(id ':' json_string) 86 | 87 | Example: 88 | 89 | `M a:"Start Suite"` - Identifies the String 'Start Suite' as 'a' in the logs 90 | `M b:"End Suite"` - Identifies the String 'End Suite' as 'b' in the logs 91 | 92 | ### T: Initial time(isoformat) 93 | 94 | Example: 95 | 96 | `T 2022-10-03T11:30:54.927` 97 | 98 | ### SS: Start Suite 99 | 100 | Spec: `name:oid, suite_id:oid, suite_source:oid, time_delta_in_seconds:float` 101 | 102 | Note: references to oid mean a reference to a previously memorized name. 103 | 104 | Note: the time may be given as -1 (if unknown -- later it may be provided 105 | through an "S" message to specify the start time which may be useful 106 | when converting to xml where the status only appears later on in the file 107 | along with the status and not at the suite definition). 108 | 109 | Example (were a, b and c are references to previously memorized names): 110 | 111 | `SS a|b|c|0.333` 112 | 113 | ## RS: Replay Start Suite 114 | 115 | Same as "SS" but used just to replay the content to specify the context 116 | when the log starts being written in a new file. 117 | 118 | ### ES: End Suite 119 | 120 | Spec: `status:oid, time_delta_in_seconds:float` 121 | 122 | Note: the status (PASS, FAIL, SKIP) is a previously memorized name. 123 | 124 | Example: 125 | 126 | `ES a|0.222` 127 | 128 | ### ST: Start Task/test 129 | 130 | Spec: `name:oid, suite_id:oid, lineno:int, time_delta_in_seconds:float` 131 | 132 | Note: the source (filename) is available through the parent suite_source. 133 | 134 | Example: 135 | 136 | `ST a|b|22|0.332` 137 | 138 | ## RT: Replay Start Task/test 139 | 140 | Same as "ST" but used just to replay the content to specify the context 141 | when the log starts being written in a new file. 142 | 143 | 144 | ### ET: End Task/Test 145 | 146 | Spec: `status:oid, message:oid, time_delta_in_seconds:float` 147 | 148 | Example: 149 | 150 | `ET a|b|0.332` 151 | 152 | ### SK: Start Keyword 153 | 154 | Spec: `name:oid, libname:oid, keyword_type:oid, doc:oid, source:oid, lineno:int, time_delta_in_seconds:float` 155 | 156 | Example: 157 | 158 | `SK a|b|c|d|e|22|0.444` 159 | 160 | ## RK: Replay Keyword 161 | 162 | Same as "SK" but used just to replay the content to specify the context 163 | when the log starts being written in a new file. 164 | 165 | ### KA: Keyword argument 166 | 167 | Spec: `argument:oid` 168 | 169 | Example: 170 | 171 | `KA f` 172 | 173 | ### AS: Assign keyword call result to a variable 174 | 175 | Spec: `assign:oid` 176 | 177 | Example: 178 | 179 | `AS f` 180 | 181 | ### EK: End Keyword 182 | 183 | Spec: `status:oid, time_delta_in_seconds:float` 184 | 185 | Example: 186 | 187 | `EK a|0.333` 188 | 189 | ### L: Provide a log message 190 | 191 | Spec: `level:level_enum, message:oid, time_delta_in_seconds:float` 192 | 193 | level_enum is: 194 | - ERROR = `E` 195 | - FAIL = `F` 196 | - INFO = `I` 197 | - WARN = `W` 198 | 199 | Example: 200 | 201 | `L E|a|0.123` 202 | 203 | ### LH: Provide an html log message 204 | 205 | Same as "L" but interned message is expected to be an html which can be 206 | embedded in the final log.html. 207 | 208 | i.e.: the message can be something as: 209 | 210 | screenshot 211 | 212 | In which case the img would be embedded as an image in the final log.html. 213 | 214 | ### S: Specify the start time (of the containing suite/test/task/keyword) 215 | 216 | Spec: `start_time_delta:float` 217 | 218 | Example: 219 | 220 | `S 2.456` 221 | 222 | ### TG: Apply tag 223 | 224 | Spec: `tag:oid` 225 | 226 | Example: 227 | 228 | `TG a` 229 | 230 | -------------------------------------------------------------------------------- /output-webview/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fg-color: var(--vscode-editor-foreground, black); 3 | --bg-color: var(--vscode-editor-background, white); 4 | --font-family: var(--vscode-editor-font-family, monospace, courier); 5 | --font-size: var(--vscode-editor-font-size, 14px); 6 | --font-weight: var(--vscode-font-weight); 7 | --menu-background: var(--vscode-menu-background, rgb(235, 235, 235)); 8 | --menu-foreground: var(--vscode-menu-foreground, rgb(0, 0, 0)); 9 | 10 | /* background-color: var(--vscode-editorError-foreground); */ 11 | /* background-color: var(--vscode-inputValidation-errorBorder); high-contrast doesn't have a good color */ 12 | /* background-color: var(--vscode-testing-iconErrored); red and green always (not always ideal...) */ 13 | --error-background-color: var(--vscode-terminalCommandDecoration-errorBackground, rgb(201, 28, 28)); 14 | --error-color: var(--vscode-button-foreground, white); 15 | 16 | --hidden-background-color: var(--vscode-foobarcolornotthere, rgb(243, 152, 16)); 17 | --hidden-color: var(--vscode-button-foreground, white); 18 | 19 | /* background-color: var(--vscode-inputValidation-infoBorder); */ 20 | /* background-color: var(--vscode-testing-iconPassed); */ 21 | --pass-background-color: var(--vscode-terminalCommandDecoration-successBackground, rgb(36, 47, 202)); 22 | --pass-color: var(--vscode-button-foreground, white); 23 | 24 | --warn-background-color: var(--vscode-debugConsole-warningForeground, rgb(190, 159, 20)); 25 | --warn-color: var(--vscode-button-foreground, white); 26 | 27 | --not-run-background-color: var(--vscode-editor-foreground, rgb(102, 102, 102)); 28 | --not-run-color: var(--vscode-editor-background, white); 29 | 30 | --summary-hover-background-color: var(--vscode-editor-hoverHighlightBackground, rgb(222, 222, 222)); 31 | 32 | /* 33 | --fg-color: white; 34 | --bg-color: #4b4a4a; 35 | --font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 36 | */ 37 | } 38 | 39 | .summaryField { 40 | margin-left: 3px; 41 | margin-right: 3px; 42 | } 43 | 44 | body { 45 | font-family: var(--font-family); 46 | font-size: var(--font-size); 47 | font-weight: var(--font-weight); 48 | color: var(--fg-color); 49 | background-color: var(--bg-color); 50 | } 51 | 52 | #mainTree { 53 | margin-top: 5px; 54 | } 55 | 56 | ul { 57 | list-style-type: none; 58 | padding-inline-start: 0px; 59 | margin-block-start: 0px; 60 | } 61 | 62 | div ul:not(:first-child) { 63 | border-left: 1px dotted rgb(141, 141, 141); 64 | } 65 | 66 | .tree li { 67 | display: block; 68 | position: relative; 69 | padding-left: 0px; 70 | } 71 | 72 | .tree ul { 73 | margin-left: 10px; 74 | padding-left: 0; 75 | } 76 | 77 | .toolbarButton { 78 | display: inline; 79 | border-radius: 5px; 80 | background: var(--bg-color); 81 | color: var(--fg-color); 82 | margin-left: 3px; 83 | width: 15px; 84 | height: 15px; 85 | border: 0; 86 | padding: 0; 87 | vertical-align: middle; 88 | } 89 | 90 | .toolbarContainer { 91 | display: inline-block; 92 | } 93 | 94 | .summaryDiv { 95 | display: inline; 96 | } 97 | 98 | details > summary { 99 | /* Note: couldn't get proper color with the svg approach */ 100 | /* list-style-image: url("data:image/svg+xml;utf8,"); */ 101 | /* list-style-type: "⮞ "; */ 102 | /* list-style-type: "⏵"; */ 103 | list-style-type: "+ "; 104 | } 105 | 106 | details[open] > summary { 107 | /* list-style-image: url("data:image/svg+xml;utf8,"); */ 108 | /* list-style-type: "⮟ "; */ 109 | /* list-style-type: "⏷"; */ 110 | list-style-type: "- "; 111 | } 112 | 113 | .NO_CHILDREN > summary { 114 | list-style-type: "  " !important; 115 | } 116 | 117 | select { 118 | background-color: var(--menu-background); 119 | color: var(--menu-foreground); 120 | } 121 | 122 | summary { 123 | padding: 3px; 124 | } 125 | 126 | a:link { 127 | color: var(--fg-color); 128 | } 129 | 130 | a:visited { 131 | color: var(--fg-color); 132 | } 133 | 134 | a:hover { 135 | color: var(--fg-color); 136 | } 137 | 138 | a:active { 139 | color: var(--fg-color); 140 | } 141 | 142 | .label { 143 | padding: 2px 2px; 144 | font-size: 0.65em; 145 | letter-spacing: 1px; 146 | white-space: nowrap; 147 | border-radius: 3px; 148 | margin-right: 5px; 149 | font-weight: bold; 150 | } 151 | 152 | .timeLabel { 153 | padding: 2px 2px; 154 | font-size: 0.85em; 155 | letter-spacing: 1px; 156 | white-space: nowrap; 157 | border-radius: 3px; 158 | margin-right: 5px; 159 | font-weight: lighter; 160 | } 161 | 162 | .label.F, 163 | .label.E, 164 | .label.FAIL, 165 | .label.ERROR { 166 | border-radius: 3px; 167 | background-color: var(--error-background-color); 168 | color: var(--error-color); 169 | font-weight: bolder; 170 | } 171 | 172 | .label.PASS, 173 | .label.I, 174 | .label.INFO { 175 | border-radius: 3px; 176 | background-color: var(--pass-background-color); 177 | color: var(--pass-color); 178 | font-weight: bolder; 179 | } 180 | 181 | .label.W, 182 | .label.WARN { 183 | border-radius: 3px; 184 | background-color: var(--warn-background-color); 185 | color: var(--warn-color); 186 | font-weight: bolder; 187 | } 188 | 189 | .label.HIDDEN { 190 | border-radius: 3px; 191 | background-color: var(--hidden-background-color); 192 | color: var(--hidden-color); 193 | font-weight: bolder; 194 | } 195 | .label.HIDDEN.inline { 196 | margin-left: 5px; 197 | } 198 | 199 | summary.HIDDEN { 200 | margin-top: 10px; 201 | margin-bottom: 10px; 202 | } 203 | 204 | .label.NOT_RUN { 205 | border-radius: 3px; 206 | background-color: var(--not-run-background-color); 207 | color: var(--not-run-color); 208 | font-weight: bolder; 209 | } 210 | 211 | #summary.FAIL, 212 | #summary.ERROR { 213 | border-bottom: 5px solid var(--error-background-color); 214 | } 215 | 216 | #summary.PASS { 217 | border-bottom: 5px solid var(--pass-background-color); 218 | } 219 | 220 | #summary.NOT_RUN { 221 | border-bottom: 5px solid var(--not-run-background-color); 222 | } 223 | 224 | /* .span_link::after { 225 | content: " ⮳"; 226 | } */ 227 | .span_link { 228 | cursor: pointer; 229 | /* text-decoration: underline; */ 230 | } 231 | 232 | summary:hover { 233 | background-color: var(--summary-hover-background-color); 234 | } 235 | -------------------------------------------------------------------------------- /output-webview/src/tree.ts: -------------------------------------------------------------------------------- 1 | // Interesting reads: 2 | // https://medium.com/metaphorical-web/javascript-treeview-controls-devil-in-the-details-74c252e00ed8 3 | // https://iamkate.com/code/tree-views/ 4 | // https://stackoverflow.com/questions/10813581/can-i-replace-the-expand-icon-of-the-details-element 5 | 6 | import { IMessage } from "./decoder"; 7 | import { saveTreeStateLater } from "./persistTree"; 8 | import { 9 | createButton, 10 | createCollapseSVG, 11 | createDetails, 12 | createDiv, 13 | createExpandSVG, 14 | createLI, 15 | createSpan, 16 | createSummary, 17 | createUL, 18 | htmlToElement, 19 | } from "./plainDom"; 20 | import { IContentAdded, ILiNodesCreated, IMessageNode, IOpts, ITreeState } from "./protocols"; 21 | 22 | export function createLiAndNodesBelow(open: boolean, liTreeId: string): ILiNodesCreated { 23 | //
  • 24 | //
    25 | // 26 | // 27 | // 28 | //
    29 | //
  • 30 | 31 | const li: HTMLLIElement = createLI(liTreeId); 32 | const details: HTMLDetailsElement = createDetails(); 33 | 34 | if (open) { 35 | details.open = open; 36 | } 37 | const summary = createSummary(); 38 | const summaryDiv = createDiv(); 39 | summaryDiv.classList.add("summaryDiv"); 40 | summary.appendChild(summaryDiv); 41 | 42 | li.appendChild(details); 43 | details.appendChild(summary); 44 | details.classList.add("NO_CHILDREN"); 45 | 46 | const span: HTMLSpanElement = createSpan(); 47 | span.setAttribute("role", "button"); 48 | summaryDiv.appendChild(span); 49 | 50 | return { li, details, summary, summaryDiv, span }; 51 | } 52 | 53 | /** 54 | * When we add content we initially add it as an item with the NO_CHILDREN class 55 | * and later we have to remove that class if it has children. 56 | */ 57 | export function addTreeContent( 58 | opts: IOpts, 59 | parent: IContentAdded, 60 | content: string, 61 | decodedMessage: IMessage, 62 | open: boolean, 63 | source: string, 64 | lineno: number, 65 | messageNode: IMessageNode, 66 | id: string 67 | ): IContentAdded { 68 | //
  • 69 | //
    70 | // 71 | // 72 | // 73 | //
      74 | //
    75 | //
    76 | //
  • 77 | 78 | const treeState: ITreeState = opts.state.runIdToTreeState[opts.runId]; 79 | const liTreeId = "li_" + id; 80 | if (treeState) { 81 | const openNodes = treeState.openNodes; 82 | if (openNodes) { 83 | open = openNodes[liTreeId]; 84 | } 85 | } 86 | const created = createLiAndNodesBelow(open, liTreeId); 87 | const li = created.li; 88 | const details = created.details; 89 | const summary = created.summary; 90 | const summaryDiv = created.summaryDiv; 91 | const span = created.span; 92 | 93 | if (decodedMessage.message_type === "LH") { 94 | const htmlContents = htmlToElement(content); 95 | span.appendChild(htmlContents); 96 | } else { 97 | span.textContent = content; 98 | } 99 | 100 | if (opts.onClickReference) { 101 | span.classList.add("span_link"); 102 | span.onclick = (ev) => { 103 | const scope = []; 104 | let p: IMessageNode = messageNode.parent; 105 | while (p !== undefined && p.message !== undefined) { 106 | scope.push(p.message); 107 | p = p.parent; 108 | } 109 | 110 | ev.preventDefault(); 111 | opts.onClickReference({ 112 | source, 113 | lineno, 114 | "message": decodedMessage.decoded, 115 | "messageType": decodedMessage.message_type, 116 | "scope": scope, 117 | }); 118 | }; 119 | } 120 | 121 | const ul = createUL("ul_" + id); 122 | details.appendChild(ul); 123 | const ret = { 124 | ul, 125 | li, 126 | details, 127 | summary, 128 | span, 129 | source, 130 | lineno, 131 | appendContentChild: undefined, 132 | decodedMessage, 133 | maxLevelFoundInHierarchy: -1, 134 | summaryDiv, 135 | }; 136 | ret["appendContentChild"] = createUlIfNeededAndAppendChild.bind(ret); 137 | parent.appendContentChild(ret); 138 | return ret; 139 | } 140 | 141 | let toolbar: HTMLSpanElement = undefined; 142 | let globalCurrMouseOver: IContentAdded = undefined; 143 | function expandOnClick() { 144 | if (globalCurrMouseOver === undefined) { 145 | return; 146 | } 147 | globalCurrMouseOver.details.open = true; 148 | for (let details of iterOverUlDetailsElements(globalCurrMouseOver.ul)) { 149 | if (!details.classList.contains("NO_CHILDREN")) { 150 | details.open = true; 151 | } 152 | } 153 | } 154 | 155 | function collapseOnClick() { 156 | if (globalCurrMouseOver === undefined) { 157 | return; 158 | } 159 | globalCurrMouseOver.details.open = false; 160 | for (let details of iterOverUlDetailsElements(globalCurrMouseOver.ul)) { 161 | details.open = false; 162 | } 163 | } 164 | 165 | function* iterOverLiDetailsElements(li: HTMLLIElement): IterableIterator { 166 | for (let child of li.childNodes) { 167 | if (child instanceof HTMLDetailsElement) { 168 | for (let c of child.childNodes) { 169 | if (c instanceof HTMLUListElement) { 170 | for (let details of iterOverUlDetailsElements(c)) { 171 | yield details; 172 | } 173 | } 174 | } 175 | yield child; 176 | } 177 | } 178 | } 179 | 180 | function* iterOverUlDetailsElements(ul: HTMLUListElement): IterableIterator { 181 | for (let child of ul.childNodes) { 182 | if (child instanceof HTMLLIElement) { 183 | for (let details of iterOverLiDetailsElements(child)) { 184 | yield details; 185 | } 186 | } 187 | } 188 | } 189 | 190 | function updateOnMouseOver(currMouseOver: IContentAdded) { 191 | if (toolbar === undefined) { 192 | toolbar = createDiv(); 193 | toolbar.classList.add("toolbarContainer"); 194 | 195 | const expand = createButton(); 196 | expand.appendChild(createExpandSVG()); 197 | expand.onclick = () => { 198 | expandOnClick(); 199 | }; 200 | expand.classList.add("toolbarButton"); 201 | 202 | const collapse = createButton(); 203 | collapse.appendChild(createCollapseSVG()); 204 | collapse.classList.add("toolbarButton"); 205 | collapse.onclick = () => { 206 | collapseOnClick(); 207 | }; 208 | toolbar.appendChild(collapse); 209 | toolbar.appendChild(expand); 210 | return; 211 | } 212 | 213 | globalCurrMouseOver = currMouseOver; 214 | currMouseOver.summaryDiv.appendChild(toolbar); 215 | } 216 | 217 | function createUlIfNeededAndAppendChild(child: IContentAdded) { 218 | const bound: IContentAdded = this; 219 | bound.ul.appendChild(child.li); 220 | if (bound.details.classList.contains("NO_CHILDREN")) { 221 | bound.details.classList.remove("NO_CHILDREN"); 222 | // If it can be toggled, track it for changes. 223 | bound.details.addEventListener("toggle", function () { 224 | saveTreeStateLater(); 225 | }); 226 | bound.summary.addEventListener("mouseover", (event) => { 227 | updateOnMouseOver(bound); 228 | }); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /dev.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a script to help with the automation of common development tasks. 3 | 4 | It requires 'fire' to be installed for the command line automation (i.e.: pip install fire). 5 | 6 | Some example commands: 7 | 8 | python -m dev set-version 0.0.2 9 | python -m dev check-tag-version 10 | """ 11 | import sys 12 | import os 13 | import traceback 14 | import subprocess 15 | 16 | __file__ = os.path.abspath(__file__) 17 | 18 | if not os.path.exists(os.path.join(os.path.abspath("."), "dev.py")): 19 | raise RuntimeError('Please execute commands from the directory containing "dev.py"') 20 | 21 | import fire 22 | 23 | 24 | try: 25 | import robot_out_stream 26 | except ImportError: 27 | # I.e.: add relative path (the cwd must be the directory containing this file). 28 | sys.path.append("src") 29 | import robot_out_stream 30 | 31 | 32 | def _fix_contents_version(contents, version): 33 | import re 34 | 35 | contents = re.sub( 36 | r"(version\s*=\s*)\"\d+\.\d+\.\d+", r'\1"%s' % (version,), contents 37 | ) 38 | contents = re.sub( 39 | r"(__version__\s*=\s*)\"\d+\.\d+\.\d+", r'\1"%s' % (version,), contents 40 | ) 41 | contents = re.sub( 42 | r"(\"version\"\s*:\s*)\"\d+\.\d+\.\d+", r'\1"%s' % (version,), contents 43 | ) 44 | contents = re.sub( 45 | r"(blob/robotframework-lsp)-\d+\.\d+\.\d+", r"\1-%s" % (version,), contents 46 | ) 47 | 48 | return contents 49 | 50 | 51 | class Dev(object): 52 | def set_version(self, version): 53 | """ 54 | Sets a new version for robotframework-output-stream in all the needed files. 55 | """ 56 | 57 | def update_version(version, filepath, fix_func=_fix_contents_version): 58 | with open(filepath, "r") as stream: 59 | contents = stream.read() 60 | 61 | new_contents = fix_func(contents, version) 62 | if contents != new_contents: 63 | print("Changed: ", filepath) 64 | with open(filepath, "w") as stream: 65 | stream.write(new_contents) 66 | 67 | update_version(version, os.path.join(".", "src", "setup.py")) 68 | update_version( 69 | version, os.path.join(".", "src", "robot_out_stream", "__init__.py") 70 | ) 71 | 72 | def get_tag(self): 73 | import subprocess 74 | 75 | # i.e.: Gets the last tagged version 76 | cmd = "git describe --tags --abbrev=0 --match robotframework-output-stream*".split() 77 | popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) 78 | stdout, stderr = popen.communicate() 79 | 80 | # Something as: b'robotframework-output-stream-0.0.1' 81 | if sys.version_info[0] >= 3: 82 | stdout = stdout.decode("utf-8") 83 | stdout = stdout.strip() 84 | return stdout 85 | 86 | def check_tag_version(self): 87 | """ 88 | Checks if the current tag matches the latest version (exits with 1 if it 89 | does not match and with 0 if it does match). 90 | """ 91 | tag = self.get_tag() 92 | version = tag[tag.rfind("-") + 1 :] 93 | 94 | if robot_out_stream.__version__ == version: 95 | sys.stderr.write("Version matches (%s) (exit(0))\n" % (version,)) 96 | sys.exit(0) 97 | else: 98 | sys.stderr.write( 99 | "Version does not match (robot out stream: %s != tag: %s) (exit(1))\n" 100 | % (robot_out_stream.__version__, version) 101 | ) 102 | sys.exit(1) 103 | 104 | def check_no_git_changes(self): 105 | output = ( 106 | subprocess.check_output(["git", "status", "-s"]) 107 | .decode("utf-8", "replace") 108 | .strip() 109 | ) 110 | if output: 111 | sys.stderr.write(f"Expected no changes in git. Found:\n{output}\n") 112 | subprocess.call(["git", "diff"]) 113 | sys.exit(1) 114 | 115 | def build_output_view(self): 116 | """ 117 | Builds the output view in prod mode in `dist`. 118 | """ 119 | import shutil 120 | import subprocess 121 | 122 | src_webview = os.path.abspath( 123 | os.path.join( 124 | os.path.dirname(__file__), 125 | "output-webview", 126 | ) 127 | ) 128 | print("=== Yarn install") 129 | shell = sys.platform == "win32" 130 | subprocess.check_call(["yarn", "install"], cwd=src_webview, shell=shell) 131 | 132 | print("=== Building with webpack") 133 | subprocess.check_call( 134 | ["yarn", "build-prod"], 135 | cwd=src_webview, 136 | shell=shell, 137 | ) 138 | 139 | index_in_dist = os.path.join(src_webview, "dist", "index.html") 140 | assert os.path.exists(index_in_dist) 141 | 142 | with open(index_in_dist, encoding="utf-8") as stream: 143 | index_contents = stream.read() 144 | 145 | # Now, let's embed the contents of the index.html into a python 146 | # module where it can be saved accordingly. 147 | 148 | index_in_src = os.path.abspath( 149 | os.path.join( 150 | os.path.dirname(__file__), 151 | "src", 152 | "robot_out_stream", 153 | "index.py", 154 | ) 155 | ) 156 | 157 | with open(index_in_src, "w", encoding="utf-8") as stream: 158 | stream.write( 159 | f"""# coding: utf-8 160 | # Note: autogenerated file. 161 | # To regenerate this file use: python -m dev build-output-view. 162 | 163 | # The INDEX_HTML_CONTENTS contains the contents of the index.html which contains 164 | # html/javascript code which can be used to visualize the contents of the 165 | # output generated by robotframework-output-stream (i.e.: the .rfstream files). 166 | 167 | INDEX_HTML_CONTENTS = {repr(index_contents)} 168 | """ 169 | ) 170 | 171 | 172 | def test_lines(): 173 | """ 174 | Check that the replace matches what we expect. 175 | 176 | Things we must match: 177 | 178 | version="0.0.1" 179 | "version": "0.0.1", 180 | __version__ = "0.0.1" 181 | https://github.com/robocorp/robotframework-lsp/blob/robotframework-lsp-0.1.1/robotframework-ls/README.md 182 | """ 183 | from robocorp_ls_core.unittest_tools.compare import compare_lines 184 | 185 | contents = _fix_contents_version( 186 | """ 187 | version="0.0.198" 188 | version = "0.0.1" 189 | "version": "0.0.1", 190 | "version":"0.0.1", 191 | "version" :"0.0.1", 192 | __version__ = "0.0.1" 193 | https://github.com/robocorp/robotframework-lsp/blob/robotframework-lsp-0.1.1/robotframework-ls/README.md 194 | """, 195 | "3.7.1", 196 | ) 197 | 198 | expected = """ 199 | version="3.7.1" 200 | version = "3.7.1" 201 | "version": "3.7.1", 202 | "version":"3.7.1", 203 | "version" :"3.7.1", 204 | __version__ = "3.7.1" 205 | https://github.com/robocorp/robotframework-lsp/blob/robotframework-lsp-3.7.1/robotframework-ls/README.md 206 | """ 207 | 208 | compare_lines(contents.splitlines(), expected.splitlines()) 209 | 210 | 211 | if __name__ == "__main__": 212 | TEST = False 213 | if TEST: 214 | test_lines() 215 | else: 216 | # Workaround so that fire always prints the output. 217 | # See: https://github.com/google/python-fire/issues/188 218 | def Display(lines, out): 219 | text = "\n".join(lines) + "\n" 220 | out.write(text) 221 | 222 | from fire import core 223 | 224 | core.Display = Display 225 | 226 | fire.Fire(Dev()) 227 | --------------------------------------------------------------------------------