Skip to content

Commit

Permalink
Robot Framework Support (#1173)
Browse files Browse the repository at this point in the history
* [stu-346-placeholder-test] feat: new Test type: "Placeholder"

* [stu-346-placeholder-test] feat: Change the path and test type of placeholder

* chore: all test step can be edited

* fix: css overflow

* chore: display error as toast

* formatting

* fix: Eslint

* chore: python formatting

* Update src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx

Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com>

* chore: wip

* chore: EsLint fix

* chore: removing dead code

* chore: fix Eslint

* chore: Eslint fix

* chore: formatting

* chore: Sequence Gallery UI

* fix: new return Result when test are duplicated doesn't close Modal. -> Fix test

* [sequencer-gallery] chore: Working import demo sequence

* [sequencer-gallery] Export & Expected Demo

* [sequencer-gallery] ui: removed weird yellow and added better comment in example

* chore: formatting

* fix: Eslint

* [stu-346-placeholder-test] chore: placeholder test => Using form

* [stu-346-placeholder-test] chore: Using object as input for better clarity

* chore: formatting

* [sequencer-gallery] chore: using test profile workflow to load example

* [sequencer-gallery] chore: remove "use" prefix as react thinks it's a hook

* [sequencer-gallery] chore: bundle example with app

* [sequencer-gallery] fix: space in sequence name

* chore: formatting

* [sequencer-gallery] chore: using process.resourcesPath

* [sequencer-gallery] chore: adding CI test for Gallery + testing injected min & max

* chore: formatting

* chore(robot): discoverer

* chore(robot): Robot import frontend - experimental

* chore(robot): robot framework => import in batches

* chore(robot): handle Robot framework in change executable

* chore(robot): formatting

* chore(robot): sample code for robot example

* chore(robot): Robot Framework Sequence Example

* chore(linter): fix E711

* chore(robot): cleanup

* chore(robot): formatting

* chore(robot): adding @itsjoeoui pattern matching recommendation

* chore(robot): exposing discoverable test type as global type

* chore(robot): formatting

* chore(robot): format

* chore(robot): fix esLint

* chore(robot): fix esLint part #2

---------

Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com>
  • Loading branch information
LatentDream and 39bytes authored Apr 25, 2024
1 parent 0ff0c5c commit 05239a5
Show file tree
Hide file tree
Showing 20 changed files with 278 additions and 77 deletions.
2 changes: 2 additions & 0 deletions captain/models/test_sequencer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class TestTypes(StrEnum):
flojoy = "flojoy"
matlab = "matlab"
placeholder = "placeholder"
robotframework = "robotframework"


class StatusTypes(StrEnum):
Expand Down Expand Up @@ -66,6 +67,7 @@ class Test(BaseModel):
max_value: Optional[float] = Field(None, alias="maxValue")
measured_value: Optional[float] = Field(None, alias="measuredValue")
unit: Optional[str] = Field(None, alias="unit")
args: Optional[List[str]] = Field(None, alias="args")


class Role(StrEnum):
Expand Down
29 changes: 25 additions & 4 deletions captain/routes/test_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import pydantic
from captain.models.pytest.pytest_models import TestDiscoverContainer
from captain.models.test_sequencer import TestSequenceRun
from captain.utils.pytest.discover_tests import discover_pytest_file
from captain.utils.pytest.discover_tests import (
discover_pytest_file,
discover_robot_file,
)
from captain.utils.config import ts_manager
from captain.utils.test_sequencer.handle_data import handle_data
from captain.utils.logger import logger
Expand Down Expand Up @@ -34,13 +37,13 @@ async def websocket_endpoint(websocket: WebSocket, socket_id: str):
logger.info(f"Client {socket_id} is disconnected")


class DiscoverPytestParams(BaseModel):
class DiscoverParams(BaseModel):
path: str
one_file: bool = Field(..., alias="oneFile")


@router.get("/discover-pytest/")
async def discover_pytest(params: DiscoverPytestParams = Depends()):
@router.get("/discover/pytest/")
async def discover_pytest(params: DiscoverParams = Depends()):
path = params.path
one_file = params.one_file
return_val, missing_lib, errors = [], [], [] # For passing info between threads
Expand All @@ -55,3 +58,21 @@ async def discover_pytest(params: DiscoverPytestParams = Depends()):
missing_libraries=missing_lib,
error=errors[0] if len(errors) > 0 else None,
)


@router.get("/discover/robot/")
async def discover_robot(params: DiscoverParams = Depends()):
path = params.path
one_file = params.one_file
return_val, errors = [], [] # For passing info between threads
thread = Thread(
target=discover_robot_file,
args=(path, one_file, return_val, errors),
)
thread.start()
thread.join()
return TestDiscoverContainer(
response=return_val,
missing_libraries=[],
error=errors[0] if len(errors) > 0 else None,
)
27 changes: 27 additions & 0 deletions captain/utils/pytest/discover_tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os
from captain.utils.logger import logger
from typing import List, Union
Expand All @@ -12,6 +13,7 @@
from captain.utils.import_utils import unload_module
import re
import pathlib
from robot.running.builder import TestSuiteBuilder


def extract_error(report: RootModel):
Expand Down Expand Up @@ -84,3 +86,28 @@ def dfs(
)
else:
return_val.extend(test_list)


def discover_robot_file(path: str, one_file: bool, return_val: list, errors: list):
try:
builder = TestSuiteBuilder()
suite = builder.build(path)
logging.info(
f"Suite: {suite} - suites in it: {suite.suites} - tests in it: {suite.tests}"
)
if one_file:
return_val.append(
TestDiscoveryResponse(
test_name=suite.full_name, path=pathlib.Path(path).as_posix()
)
)
else:
for test in suite.tests:
return_val.append(
TestDiscoveryResponse(
test_name=test.full_name, path=pathlib.Path(path).as_posix()
)
)

except Exception as e:
errors.append(str(e))
39 changes: 39 additions & 0 deletions captain/utils/test_sequencer/run_test_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,44 @@ def _run_placeholder(node: TestNode) -> Extract:
)


@_with_stream_test_result
def _run_robotframework(node: TestNode) -> Extract:
"""
runs python file.
@params file_path: path to the file
@returns:
bool: result of the test
float: time taken to execute the test
str: error message if any
"""
start_time = time.time()
logger.info(f"[Robot Framework Runner] Running {node.path}")
if node.args is not None:
cmd = ["robot", "--test", *node.args, node.path]
else:
cmd = ["robot", node.path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(f"[Robot Framework Runner] Running {result}")
end_time = time.time()
if result.returncode == 0:
is_pass = True
else:
logger.info(
f"TEST {node.path} FAILED:\nSTDOUT: {result.stdout.decode()}\nSTDERR: {result.stderr.decode()}"
)
is_pass = False
return (
lambda _: None,
TestResult(
node,
is_pass,
end_time - start_time,
result.stderr.decode() if not is_pass else None,
utcnow_str(),
),
)


def _eval_condition(
result_dict: dict[str, TestResult], condition: str, identifiers: set[str]
):
Expand Down Expand Up @@ -266,6 +304,7 @@ def get_next_children_from_context(context: Context):
TestTypes.python: (None, _run_python),
TestTypes.pytest: (None, _run_pytest),
TestTypes.placeholder: (None, _run_placeholder),
TestTypes.robotframework: (None, _run_robotframework),
},
),
"conditional": (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}}
19 changes: 19 additions & 0 deletions examples/test-sequencer-robot-framework-example/TestExample.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
*** Settings ***
Library flojoy_cloud.test_sequencer
Library calculate.py

*** Test Cases ***
TEST EXPORT
${result} Calculate 3 + 1
# Export the `result` so it's display in the sequencer
# + this value will be upload to Flojoy Cloud
Export ${result}
Should Not Be Equal 4 ${result}

TEST ASSERT
${result} Calculate 1 + 1
Export ${result}
# Call the `is_in_range` from the test_sequencer
${ok} Is In Range ${result}
Should Be True ${ok}

5 changes: 5 additions & 0 deletions examples/test-sequencer-robot-framework-example/calculate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def calculate(term):
if term == "":
return 0
else:
return eval(term)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
psutil==5.9.8
16 changes: 13 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ nimodinst = "^1.4.7"
flojoy-cloud = "^0.2.1"
bcrypt = "^4.1.2"
tinymovr = "^1.6.5"
robotframework = "^7.0"

[tool.poetry.group.blocks.dependencies]
scikit-image = "^0.22.0"
Expand Down Expand Up @@ -75,6 +76,7 @@ optional = true
scikit-learn = "^1.3.2"

[tool.poetry.group.user.dependencies]
psutil = "5.9.8"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion src/main/ipc-main-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export const registerIpcMainHandlers = () => {

ipcMain.handle(
API.openTestPicker,
async (e) => await openFilePicker(e, "Test", ["json", "py"]),
async (e) => await openFilePicker(e, "Test", ["json", "py", "robot"]),
);
ipcMain.handle(API.openEditorWindow, (_, filepath) => {
createEditorWindow(filepath);
Expand Down
63 changes: 44 additions & 19 deletions src/renderer/hooks/useTestImport.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { TestDiscoverContainer } from "@/renderer/types/test-sequencer";
import {
TestDiscoverContainer,
} from "@/renderer/types/test-sequencer";
import {
createNewTest,
useDisplayedSequenceState,
} from "./useTestSequencerState";
import { map } from "lodash";
import { ImportTestSettings } from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal";
import {
ImportTestSettings,
discoverableTestTypes as DiscoverableTestTypes,
} from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal";
import { toast } from "sonner";
import { useCallback } from "react";
import { discoverPytest } from "@/renderer/lib/api";
import { discoverPytest, discoverRobot } from "@/renderer/lib/api";
import { useSequencerModalStore } from "../stores/modal";
import { toastResultPromise } from "../utils/report-error";
import { Result, err, ok } from "neverthrow";
import { match } from "ts-pattern";

function parseDiscoverContainer(
data: TestDiscoverContainer,
Expand All @@ -21,6 +27,10 @@ function parseDiscoverContainer(
name: container.testName,
path: container.path,
type: settings.importType,
args:
settings.importType === "robotframework" && !settings.importAsOneRef
? [container.testName]
: undefined,
});
return new_elem;
});
Expand All @@ -47,20 +57,27 @@ export const useDiscoverAndImportTests = () => {
settings: ImportTestSettings,
setModalOpen: (val: boolean) => void,
): Promise<Result<void, Error>> {
let data: TestDiscoverContainer;
if (settings.importType == "python") {
data = {
response: [{ testName: path, path: path }],
missingLibraries: [],
error: null,
};
} else {
const res = await discoverPytest(path, settings.importAsOneRef);
if (res.isErr()) {
return err(res.error);
}
data = res.value;
const dataResponse = await match(settings.importType)
.with("python", async () => {
return ok({
response: [{ testName: path, path: path }],
missingLibraries: [],
error: null,
});
})
.with(
"pytest",
async () => await discoverPytest(path, settings.importAsOneRef),
)
.with(
"robotframework",
async () => await discoverRobot(path, settings.importAsOneRef),
)
.exhaustive();
if (dataResponse.isErr()) {
return err(dataResponse.error);
}
const data = dataResponse.value;
if (data.error) {
return err(Error(data.error));
}
Expand Down Expand Up @@ -126,7 +143,7 @@ export const useDiscoverAndImportTests = () => {
return openFilePicker;
};

export const useDiscoverPytestElements = () => {
export const useDiscoverElements = () => {
const handleUserDepInstall = useCallback(async (depName: string) => {
const promise = () => window.api.poetryInstallDepUserGroup(depName);
toast.promise(promise, {
Expand All @@ -140,7 +157,15 @@ export const useDiscoverPytestElements = () => {
}, []);

async function getTests(path: string) {
const res = await discoverPytest(path, false);
let res: Result<TestDiscoverContainer, Error>;
let type: DiscoverableTestTypes;
if (path.endsWith(".robot")) {
res = await discoverRobot(path, false);
type = "robotframework";
} else {
res = await discoverPytest(path, false);
type = "pytest";
}
if (res.isErr()) {
return err(res.error);
}
Expand All @@ -161,7 +186,7 @@ export const useDiscoverPytestElements = () => {
}
const newElems = parseDiscoverContainer(data, {
importAsOneRef: false,
importType: "pytest",
importType: type,
});
if (newElems.length === 0) {
return err(Error("No tests were found in the specified file."));
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/hooks/useTestSequencerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const NewTest = z.object({
minValue: z.number().optional(),
maxValue: z.number().optional(),
unit: z.string().optional(),
args: z.string().array().optional(),
});

export type NewTest = z.infer<typeof NewTest>;
Expand All @@ -131,6 +132,7 @@ export function createNewTest(test: NewTest): Test {
minValue: test.minValue,
maxValue: test.maxValue,
unit: test.unit,
args: test.args,
};
return newTest;
}
Expand Down
Loading

0 comments on commit 05239a5

Please sign in to comment.