Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Robot Framework Support #1173

Merged
merged 60 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
104497b
[stu-346-placeholder-test] feat: new Test type: "Placeholder"
LatentDream Apr 17, 2024
97b030c
[stu-346-placeholder-test] feat: Change the path and test type of pla…
LatentDream Apr 17, 2024
8d88709
chore: all test step can be edited
LatentDream Apr 17, 2024
4dc4795
Merge branch 'main' into stu-346-placeholder-test
LatentDream Apr 17, 2024
7180c82
fix: css overflow
LatentDream Apr 17, 2024
79a706d
chore: display error as toast
LatentDream Apr 17, 2024
1c11d96
formatting
LatentDream Apr 17, 2024
fde3542
fix: Eslint
LatentDream Apr 17, 2024
68361b0
chore: python formatting
LatentDream Apr 17, 2024
07221df
Merge branch 'main' into stu-346-placeholder-test
39bytes Apr 17, 2024
be7f5ca
Update src/renderer/routes/test_sequencer_panel/components/modals/Cha…
LatentDream Apr 17, 2024
e22897d
Merge branch 'main' into stu-346-placeholder-test
LatentDream Apr 18, 2024
6a25bfb
chore: wip
LatentDream Apr 19, 2024
c8e8a14
chore: EsLint fix
LatentDream Apr 20, 2024
87e9d12
chore: removing dead code
LatentDream Apr 20, 2024
702512e
chore: fix Eslint
LatentDream Apr 20, 2024
781b9fd
chore: Eslint fix
LatentDream Apr 20, 2024
9cbcd8c
chore: formatting
LatentDream Apr 20, 2024
2187638
chore: Sequence Gallery UI
LatentDream Apr 20, 2024
c1f0de1
fix: new return Result when test are duplicated doesn't close Modal. …
LatentDream Apr 20, 2024
0ab4c07
[sequencer-gallery] chore: Working import demo sequence
LatentDream Apr 20, 2024
f4fd870
[sequencer-gallery] Export & Expected Demo
LatentDream Apr 20, 2024
f35faa3
[sequencer-gallery] ui: removed weird yellow and added better comment…
LatentDream Apr 20, 2024
ad14376
chore: formatting
LatentDream Apr 20, 2024
773758c
fix: Eslint
LatentDream Apr 20, 2024
747c2a1
[stu-346-placeholder-test] chore: placeholder test => Using form
LatentDream Apr 21, 2024
c776372
[stu-346-placeholder-test] chore: Using object as input for better cl…
LatentDream Apr 21, 2024
407976f
chore: formatting
LatentDream Apr 21, 2024
6a3ac11
Merge branch 'main' into stu-346-placeholder-test
LatentDream Apr 21, 2024
b4d24b2
[sequencer-gallery] chore: using test profile workflow to load example
LatentDream Apr 21, 2024
35fabe1
[sequencer-gallery] chore: remove "use" prefix as react thinks it's a…
LatentDream Apr 21, 2024
55ac040
Merge branch 'main' into stu-346-placeholder-test
LatentDream Apr 22, 2024
fe82571
[sequencer-gallery] chore: bundle example with app
LatentDream Apr 22, 2024
fbdfad9
[sequencer-gallery] fix: space in sequence name
LatentDream Apr 22, 2024
932bf87
chore: formatting
LatentDream Apr 22, 2024
97c6164
Merge branch 'main' into sequencer-gallery
LatentDream Apr 22, 2024
0c4a161
[sequencer-gallery] chore: using process.resourcesPath
LatentDream Apr 22, 2024
f9ea663
[sequencer-gallery] chore: adding CI test for Gallery + testing injec…
LatentDream Apr 22, 2024
054fab7
chore: formatting
LatentDream Apr 22, 2024
fafe621
chore(robot): discoverer
LatentDream Apr 23, 2024
111b37e
chore(robot): Robot import frontend - experimental
LatentDream Apr 23, 2024
b82177b
chore(robot): robot framework => import in batches
LatentDream Apr 23, 2024
20ccc4e
chore(robot): handle Robot framework in change executable
LatentDream Apr 23, 2024
626d397
chore(robot): formatting
LatentDream Apr 23, 2024
34de28d
chore(robot): sample code for robot example
LatentDream Apr 24, 2024
1a3a1dd
chore(robot): merge sequence gallery
LatentDream Apr 24, 2024
7c59be7
chore(robot): Robot Framework Sequence Example
LatentDream Apr 24, 2024
f2e1388
chore(linter): fix E711
LatentDream Apr 24, 2024
a6f42d8
chore(robot): merge with lastest placeholder change
LatentDream Apr 24, 2024
1ccb7a7
chore(robot): cleanup
LatentDream Apr 24, 2024
4efb9f9
chore(robot): formatting
LatentDream Apr 24, 2024
4f1bddc
chore(robot): adding @itsjoeoui pattern matching recommendation
LatentDream Apr 25, 2024
2a7ea6f
chore(robot): exposing discoverable test type as global type
LatentDream Apr 25, 2024
df8c784
chore(robot): merge with latest main
LatentDream Apr 25, 2024
8cd66e9
chore(robot): formatting
LatentDream Apr 25, 2024
d049f1f
chore(robot): merge with latest change
LatentDream Apr 25, 2024
3f3287d
chore(robot): format
LatentDream Apr 25, 2024
726d99c
chore(robot): fix esLint
LatentDream Apr 25, 2024
4358c15
chore(robot): fix esLint part #2
LatentDream Apr 25, 2024
6cfa362
Merge branch 'main' into robotframework-prototype
39bytes Apr 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading