Module: Mesa
Branch: main
Commit: 60cd0af06c081a6762d0598a9dfbbfc37c2b65d3
URL:    
http://cgit.freedesktop.org/mesa/mesa/commit/?id=60cd0af06c081a6762d0598a9dfbbfc37c2b65d3

Author: Guilherme Gallo <guilherme.ga...@collabora.com>
Date:   Thu Oct 26 08:54:54 2023 -0300

ci/lava: Add unit tests covering job definition

Add two unit tests related to the LAVA job definition.

test_generate_lava_job_definition_sanity checks for the most important
fields, deploy actions, namespaces etc.

test_lava_job_definition compares the generated definition with static
skeleton YAML files committed inside tests/data folder.

Signed-off-by: Guilherme Gallo <guilherme.ga...@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/25912>

---

 .gitlab-ci/lava/utils/ssh_job_definition.py        |   1 -
 .../FASTBOOT_force_uart=False_job_definition.yaml  | 142 +++++++++++++++
 .../FASTBOOT_force_uart=True_job_definition.yaml   |  96 ++++++++++
 .../UBOOT_force_uart=False_job_definition.yaml     | 114 ++++++++++++
 .../data/UBOOT_force_uart=True_job_definition.yaml |  70 ++++++++
 .gitlab-ci/tests/utils/test_lava_job_definition.py | 197 +++++++++++++++++++++
 6 files changed, 619 insertions(+), 1 deletion(-)

diff --git a/.gitlab-ci/lava/utils/ssh_job_definition.py 
b/.gitlab-ci/lava/utils/ssh_job_definition.py
index e3bfad1ba96..eae8f6c2328 100644
--- a/.gitlab-ci/lava/utils/ssh_job_definition.py
+++ b/.gitlab-ci/lava/utils/ssh_job_definition.py
@@ -181,7 +181,6 @@ def wrap_final_deploy_action(final_deploy_action: dict):
         "namespace": "dut",
         "failure_retry": NUMBER_OF_ATTEMPTS_LAVA_BOOT,
         "timeout": {"minutes": 10},
-        "timeouts": {"http-download": {"minutes": 2}},
     }
 
     final_deploy_action.update(wrap)
diff --git 
a/.gitlab-ci/tests/data/FASTBOOT_force_uart=False_job_definition.yaml 
b/.gitlab-ci/tests/data/FASTBOOT_force_uart=False_job_definition.yaml
new file mode 100644
index 00000000000..b2b5ee7e1f9
--- /dev/null
+++ b/.gitlab-ci/tests/data/FASTBOOT_force_uart=False_job_definition.yaml
@@ -0,0 +1,142 @@
+job_name: 'test-project: my_pipeline_info'
+device_type: my_fastboot_device_type
+visibility:
+  group:
+  - my_visibility_group
+priority: 75
+context:
+  extra_nfsroot_args: ' init=/init rootwait usbcore.quirks=0bda:8153:k'
+timeouts:
+  job:
+    minutes: 10
+  actions:
+    depthcharge-retry:
+      minutes: 4
+    depthcharge-start:
+      minutes: 1
+    depthcharge-action:
+      minutes: 15
+actions:
+- deploy:
+    timeout:
+      minutes: 10
+    to: nfs
+    nfsrootfs:
+      url: None/lava-rootfs.tar.zst
+      compression: zstd
+    namespace: dut
+- deploy:
+    timeout:
+      minutes: 5
+    to: downloads
+    os: oe
+    images:
+      kernel:
+        url: None/None
+      dtb:
+        url: None/my_dtb_filename.dtb
+    postprocess:
+      docker:
+        image: registry.gitlab.collabora.com/lava/health-check-docker
+        steps:
+        - cat Image.gz my_dtb_filename.dtb > Image.gz+dtb
+        - mkbootimg --kernel Image.gz+dtb --cmdline "root=/dev/nfs rw 
nfsroot=$NFS_SERVER_IP:$NFS_ROOTFS,tcp,hard
+          rootwait ip=dhcp init=/init" --pagesize 4096 --base 0x80000000 -o 
boot.img
+    namespace: dut
+- deploy:
+    timeout:
+      minutes: 10
+    to: fastboot
+    docker:
+      image: registry.gitlab.collabora.com/lava/health-check-docker
+    images:
+      boot:
+        url: downloads://boot.img
+    namespace: dut
+    failure_retry: 3
+- boot:
+    timeout:
+      minutes: 2
+    docker:
+      image: registry.gitlab.collabora.com/lava/health-check-docker
+    failure_retry: 3
+    method: fastboot
+    prompts:
+    - 'lava-shell:'
+    commands:
+    - set_active a
+    namespace: dut
+    auto_login:
+      login_commands:
+      - dropbear -R -B
+      - touch /dut_ready
+      login_prompt: 'ogin:'
+      username: ''
+- test:
+    namespace: dut
+    definitions:
+    - from: inline
+      name: setup-ssh-server
+      path: inline-setup-ssh-server
+      repository:
+        metadata:
+          format: Lava-Test Test Definition 1.0
+          name: dut-env-export
+        run:
+          steps:
+          - |-
+            echo test FASTBOOT
+          - export -p > /dut-env-vars.sh
+- test:
+    namespace: container
+    timeout:
+      minutes: 10
+    failure_retry: 3
+    definitions:
+    - name: docker_ssh_client
+      from: inline
+      path: inline/docker_ssh_client.yaml
+      repository:
+        metadata:
+          name: mesa
+          description: Mesa test plan
+          format: Lava-Test Test Definition 1.0
+        run:
+          steps:
+          - |-
+            set -ex
+            timeout 1m bash << EOF
+            while [ -z "$(lava-target-ip)" ]; do
+                echo Waiting for DUT to join LAN;
+                sleep 1;
+            done
+            EOF
+
+            ping -c 5 -w 60 $(lava-target-ip)
+
+            lava_ssh_test_case() {
+                set -x
+                local test_case="${1}"
+                shift
+                lava-test-case "${test_case}" --shell \
+                    ssh ${SSH_PTY_ARGS:--T} \
+                    -o StrictHostKeyChecking=no -o 
UserKnownHostsFile=/dev/null \
+                    root@$(lava-target-ip) "${@}"
+            }
+          - lava_ssh_test_case 'wait_for_dut_login' << EOF
+          - while [ ! -e /dut_ready ]; do sleep 1; done;
+          - EOF
+          - |-
+            lava_ssh_test_case 'artifact_download' 'bash --' << EOF
+            source /dut-env-vars.sh
+            set -ex
+            curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar -xz -C /
+            mkdir -p /ci/project/dir
+            curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar --zstd -x -C /ci/project/dir
+            echo Could not find jwt file, disabling S3 requests...
+            sed -i '/S3_RESULTS_UPLOAD/d' /set-job-env-vars.sh
+            EOF
+          - export SSH_PTY_ARGS=-tt
+          - lava_ssh_test_case 'test-project_dut' '"cd / && /init-stage2.sh"'
+    docker:
+      image:
diff --git a/.gitlab-ci/tests/data/FASTBOOT_force_uart=True_job_definition.yaml 
b/.gitlab-ci/tests/data/FASTBOOT_force_uart=True_job_definition.yaml
new file mode 100644
index 00000000000..65b817d805e
--- /dev/null
+++ b/.gitlab-ci/tests/data/FASTBOOT_force_uart=True_job_definition.yaml
@@ -0,0 +1,96 @@
+job_name: 'test-project: my_pipeline_info'
+device_type: my_fastboot_device_type
+visibility:
+  group:
+  - my_visibility_group
+priority: 75
+context:
+  extra_nfsroot_args: ' init=/init rootwait usbcore.quirks=0bda:8153:k'
+timeouts:
+  job:
+    minutes: 10
+  actions:
+    depthcharge-retry:
+      minutes: 4
+    depthcharge-start:
+      minutes: 1
+    depthcharge-action:
+      minutes: 15
+actions:
+- deploy:
+    timeout:
+      minutes: 10
+    to: nfs
+    nfsrootfs:
+      url: None/lava-rootfs.tar.zst
+      compression: zstd
+- deploy:
+    timeout:
+      minutes: 5
+    to: downloads
+    os: oe
+    images:
+      kernel:
+        url: None/None
+      dtb:
+        url: None/my_dtb_filename.dtb
+    postprocess:
+      docker:
+        image: registry.gitlab.collabora.com/lava/health-check-docker
+        steps:
+        - cat Image.gz my_dtb_filename.dtb > Image.gz+dtb
+        - mkbootimg --kernel Image.gz+dtb --cmdline "root=/dev/nfs rw 
nfsroot=$NFS_SERVER_IP:$NFS_ROOTFS,tcp,hard
+          rootwait ip=dhcp init=/init" --pagesize 4096 --base 0x80000000 -o 
boot.img
+- deploy:
+    timeout:
+      minutes: 2
+    to: fastboot
+    docker:
+      image: registry.gitlab.collabora.com/lava/health-check-docker
+    images:
+      boot:
+        url: downloads://boot.img
+- boot:
+    timeout:
+      minutes: 2
+    docker:
+      image: registry.gitlab.collabora.com/lava/health-check-docker
+    failure_retry: 3
+    method: fastboot
+    prompts:
+    - 'lava-shell:'
+    commands:
+    - set_active a
+- test:
+    timeout:
+      minutes: 10
+    failure_retry: 1
+    definitions:
+    - name: mesa
+      from: inline
+      lava-signal: kmsg
+      path: inline/mesa.yaml
+      repository:
+        metadata:
+          name: mesa
+          description: Mesa test plan
+          os:
+          - oe
+          scope:
+          - functional
+          format: Lava-Test Test Definition 1.0
+        run:
+          steps:
+          - echo test FASTBOOT
+          - set -ex
+          - curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar -xz
+            -C /
+          - mkdir -p /ci/project/dir
+          - curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar --zstd
+            -x -C /ci/project/dir
+          - echo Could not find jwt file, disabling S3 requests...
+          - sed -i '/S3_RESULTS_UPLOAD/d' /set-job-env-vars.sh
+          - mkdir -p /ci/project/dir
+          - curl None | tar --zstd -x -C /ci/project/dir
+          - sleep 1
+          - lava-test-case 'test-project_dut' --shell /init-stage2.sh
diff --git a/.gitlab-ci/tests/data/UBOOT_force_uart=False_job_definition.yaml 
b/.gitlab-ci/tests/data/UBOOT_force_uart=False_job_definition.yaml
new file mode 100644
index 00000000000..721eba24c2f
--- /dev/null
+++ b/.gitlab-ci/tests/data/UBOOT_force_uart=False_job_definition.yaml
@@ -0,0 +1,114 @@
+job_name: 'test-project: my_pipeline_info'
+device_type: my_uboot_device_type
+visibility:
+  group:
+  - my_visibility_group
+priority: 75
+context:
+  extra_nfsroot_args: ' init=/init rootwait usbcore.quirks=0bda:8153:k'
+timeouts:
+  job:
+    minutes: 10
+  actions:
+    depthcharge-retry:
+      minutes: 4
+    depthcharge-start:
+      minutes: 1
+    depthcharge-action:
+      minutes: 15
+actions:
+- deploy:
+    timeout:
+      minutes: 10
+    to: tftp
+    os: oe
+    kernel:
+      url: None/None
+    nfsrootfs:
+      url: None/lava-rootfs.tar.zst
+      compression: zstd
+    dtb:
+      url: None/my_dtb_filename.dtb
+    namespace: dut
+    failure_retry: 3
+- boot:
+    failure_retry: 3
+    method: u-boot
+    prompts:
+    - 'lava-shell:'
+    commands: nfs
+    namespace: dut
+    auto_login:
+      login_commands:
+      - dropbear -R -B
+      - touch /dut_ready
+      login_prompt: 'ogin:'
+      username: ''
+- test:
+    namespace: dut
+    definitions:
+    - from: inline
+      name: setup-ssh-server
+      path: inline-setup-ssh-server
+      repository:
+        metadata:
+          format: Lava-Test Test Definition 1.0
+          name: dut-env-export
+        run:
+          steps:
+          - |-
+            echo test UBOOT
+          - export -p > /dut-env-vars.sh
+- test:
+    namespace: container
+    timeout:
+      minutes: 10
+    failure_retry: 3
+    definitions:
+    - name: docker_ssh_client
+      from: inline
+      path: inline/docker_ssh_client.yaml
+      repository:
+        metadata:
+          name: mesa
+          description: Mesa test plan
+          format: Lava-Test Test Definition 1.0
+        run:
+          steps:
+          - |-
+            set -ex
+            timeout 1m bash << EOF
+            while [ -z "$(lava-target-ip)" ]; do
+                echo Waiting for DUT to join LAN;
+                sleep 1;
+            done
+            EOF
+
+            ping -c 5 -w 60 $(lava-target-ip)
+
+            lava_ssh_test_case() {
+                set -x
+                local test_case="${1}"
+                shift
+                lava-test-case "${test_case}" --shell \
+                    ssh ${SSH_PTY_ARGS:--T} \
+                    -o StrictHostKeyChecking=no -o 
UserKnownHostsFile=/dev/null \
+                    root@$(lava-target-ip) "${@}"
+            }
+          - lava_ssh_test_case 'wait_for_dut_login' << EOF
+          - while [ ! -e /dut_ready ]; do sleep 1; done;
+          - EOF
+          - |-
+            lava_ssh_test_case 'artifact_download' 'bash --' << EOF
+            source /dut-env-vars.sh
+            set -ex
+            curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar -xz -C /
+            mkdir -p /ci/project/dir
+            curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar --zstd -x -C /ci/project/dir
+            echo Could not find jwt file, disabling S3 requests...
+            sed -i '/S3_RESULTS_UPLOAD/d' /set-job-env-vars.sh
+            EOF
+          - export SSH_PTY_ARGS=-tt
+          - lava_ssh_test_case 'test-project_dut' '"cd / && /init-stage2.sh"'
+    docker:
+      image:
diff --git a/.gitlab-ci/tests/data/UBOOT_force_uart=True_job_definition.yaml 
b/.gitlab-ci/tests/data/UBOOT_force_uart=True_job_definition.yaml
new file mode 100644
index 00000000000..38ae766b594
--- /dev/null
+++ b/.gitlab-ci/tests/data/UBOOT_force_uart=True_job_definition.yaml
@@ -0,0 +1,70 @@
+job_name: 'test-project: my_pipeline_info'
+device_type: my_uboot_device_type
+visibility:
+  group:
+  - my_visibility_group
+priority: 75
+context:
+  extra_nfsroot_args: ' init=/init rootwait usbcore.quirks=0bda:8153:k'
+timeouts:
+  job:
+    minutes: 10
+  actions:
+    depthcharge-retry:
+      minutes: 4
+    depthcharge-start:
+      minutes: 1
+    depthcharge-action:
+      minutes: 15
+actions:
+- deploy:
+    timeout:
+      minutes: 5
+    to: tftp
+    os: oe
+    kernel:
+      url: None/None
+    nfsrootfs:
+      url: None/lava-rootfs.tar.zst
+      compression: zstd
+    dtb:
+      url: None/my_dtb_filename.dtb
+- boot:
+    failure_retry: 3
+    method: u-boot
+    prompts:
+    - 'lava-shell:'
+    commands: nfs
+- test:
+    timeout:
+      minutes: 10
+    failure_retry: 1
+    definitions:
+    - name: mesa
+      from: inline
+      lava-signal: kmsg
+      path: inline/mesa.yaml
+      repository:
+        metadata:
+          name: mesa
+          description: Mesa test plan
+          os:
+          - oe
+          scope:
+          - functional
+          format: Lava-Test Test Definition 1.0
+        run:
+          steps:
+          - echo test UBOOT
+          - set -ex
+          - curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar -xz
+            -C /
+          - mkdir -p /ci/project/dir
+          - curl -L --retry 4 -f --retry-all-errors --retry-delay 60 None | 
tar --zstd
+            -x -C /ci/project/dir
+          - echo Could not find jwt file, disabling S3 requests...
+          - sed -i '/S3_RESULTS_UPLOAD/d' /set-job-env-vars.sh
+          - mkdir -p /ci/project/dir
+          - curl None | tar --zstd -x -C /ci/project/dir
+          - sleep 1
+          - lava-test-case 'test-project_dut' --shell /init-stage2.sh
diff --git a/.gitlab-ci/tests/utils/test_lava_job_definition.py 
b/.gitlab-ci/tests/utils/test_lava_job_definition.py
new file mode 100644
index 00000000000..a02378a06d1
--- /dev/null
+++ b/.gitlab-ci/tests/utils/test_lava_job_definition.py
@@ -0,0 +1,197 @@
+import importlib
+import os
+import re
+from itertools import chain
+from pathlib import Path
+from typing import Any, Iterable, Literal
+from unittest import mock
+
+import lava.utils.constants
+import pytest
+from lava.lava_job_submitter import LAVAJobSubmitter
+from lava.utils.lava_job_definition import LAVAJobDefinition
+from ruamel.yaml import YAML
+
+
+def flatten(iterable: Iterable[Iterable[Any]]) -> list[Any]:
+    return list(chain.from_iterable(iterable))
+
+
+# mock shell file
+@pytest.fixture(scope="session")
+def shell_file(tmp_path_factory):
+    def create_shell_file(content: str = "# test"):
+        shell_file = tmp_path_factory.mktemp("data") / "shell_file.sh"
+        shell_file.write_text(content)
+        return shell_file
+
+    return create_shell_file
+
+
+# fn to load the data file from $CWD/data using pathlib
+def load_data_file(filename):
+    return Path(__file__).parent.parent / "data" / filename
+
+
+def load_yaml_file(filename) -> dict:
+    with open(load_data_file(filename)) as f:
+        return YAML().load(f)
+
+
+def job_submitter_factory(mode: Literal["UBOOT", "FASTBOOT"], shell_file):
+    if mode == "UBOOT":
+        boot_method = "u-boot"
+        device_type = "my_uboot_device_type"
+    elif mode == "FASTBOOT":
+        boot_method = "fastboot"
+        device_type = "my_fastboot_device_type"
+
+    job_timeout_min = 10
+    mesa_job_name = "dut test"
+    pipeline_info = "my_pipeline_info"
+    project_name = "test-project"
+    visibility_group = "my_visibility_group"
+
+    return LAVAJobSubmitter(
+        boot_method=boot_method,
+        ci_project_dir="/ci/project/dir",
+        device_type=device_type,
+        dtb_filename="my_dtb_filename",
+        first_stage_init=shell_file,
+        job_timeout_min=job_timeout_min,
+        mesa_job_name=mesa_job_name,
+        pipeline_info=pipeline_info,
+        visibility_group=visibility_group,
+        project_name=project_name,
+    )
+
+
+@pytest.fixture
+def clear_env_vars(autouse=True):
+    with mock.patch.dict(os.environ) as environ:
+        # Remove all LAVA-related environment variables to make the test more 
robust
+        # and deterministic, once a envvar is capable of overriding the 
default value
+        for key in environ:
+            if any(kw in key for kw in ("LAVA_", "CI_", "JOB_", "RUNNER_", 
"DEVICE_")):
+                del environ[key]
+        # reload lava.utils.constants to update the JOB_PRIORITY value
+        importlib.reload(lava.utils.constants)
+        importlib.reload(lava.utils.lava_job_definition)
+        yield
+
+
+@pytest.fixture
+def mock_collabora_farm(clear_env_vars, monkeypatch):
+    # Mock a Collabora farm-like device runner tag to enable SSH execution
+    monkeypatch.setenv("RUNNER_TAG", "mesa-ci-1234-lava-collabora")
+
+
+@pytest.mark.parametrize("force_uart", [True, False], ids=["SSH", "UART"])
+@pytest.mark.parametrize("mode", ["UBOOT", "FASTBOOT"])
+def test_generate_lava_job_definition_sanity(
+    force_uart, mode, shell_file, mock_collabora_farm, monkeypatch
+):
+    monkeypatch.setattr(lava.utils.lava_job_definition, "FORCE_UART", 
force_uart)
+
+    init_script_content = f"echo test {mode}"
+    job_submitter = job_submitter_factory(mode, 
shell_file(init_script_content))
+    job_definition = 
LAVAJobDefinition(job_submitter).generate_lava_job_definition()
+
+    # Load the YAML output and check that it contains the expected keys and 
values
+    yaml = YAML()
+    job_dict = yaml.load(job_definition)
+    yaml.dump(job_dict, 
Path(f"/tmp/{mode}_force_uart={force_uart}_job_definition.yaml"))
+    assert job_dict["device_type"] == job_submitter.device_type
+    assert job_dict["visibility"]["group"] == [job_submitter.visibility_group]
+    assert job_dict["timeouts"]["job"]["minutes"] == 
job_submitter.job_timeout_min
+    assert job_dict["context"]["extra_nfsroot_args"]
+    assert job_dict["timeouts"]["actions"]
+
+    assert len(job_dict["actions"]) == 3 if mode == "UART" else 5
+
+    last_test_action = job_dict["actions"][-1]["test"]
+    # TODO: Remove hardcoded "mesa" test name, as this submitter is being used 
by other projects
+    first_test_name = last_test_action["definitions"][0]["name"]
+    is_running_ssh = "ssh" in first_test_name
+    # if force_uart, is_ssh must be False. If is_ssh, force_uart must be 
False. Both can be False
+    assert not (is_running_ssh and force_uart)
+    assert last_test_action["failure_retry"] == 3 if is_running_ssh else 1
+
+    run_steps = 
"".join(last_test_action["definitions"][0]["repository"]["run"]["steps"])
+    # Check for project name in lava-test-case
+    assert re.search(rf"lava.?\S*.test.case.*{job_submitter.project_name}", 
run_steps)
+
+    action_names = flatten(j.keys() for j in job_dict["actions"])
+    if is_running_ssh:
+        assert action_names == (
+            [
+                "deploy",
+                "boot",
+                "test",  # DUT: SSH server
+                "test",  # Docker: SSH client
+            ]
+            if mode == "UBOOT"
+            else [
+                "deploy",  # NFS
+                "deploy",  # Image generation
+                "deploy",  # Image deployment
+                "boot",
+                "test",  # DUT: SSH server
+                "test",  # Docker: SSH client
+            ]
+        )
+        test_action_server = job_dict["actions"][-2]["test"]
+        # SSH server in the DUT
+        assert test_action_server["namespace"] == "dut"
+        # SSH client via docker
+        assert last_test_action["namespace"] == "container"
+
+        boot_action = next(a["boot"] for a in job_dict["actions"] if "boot" in 
a)
+        assert boot_action["namespace"] == "dut"
+
+        # SSH server bootstrapping
+        assert "dropbear" in 
"".join(boot_action["auto_login"]["login_commands"])
+        return
+
+    # ---- Not SSH job
+    assert action_names == (
+        [
+            "deploy",
+            "boot",
+            "test",
+        ]
+        if mode == "UBOOT"
+        else [
+            "deploy",  # NFS
+            "deploy",  # Image generation
+            "deploy",  # Image deployment
+            "boot",
+            "test",
+        ]
+    )
+    assert init_script_content in run_steps
+
+
+# use yaml files from tests/data/ to test the job definition generation
+@pytest.mark.parametrize("force_uart", [False, True], ids=["SSH", "UART"])
+@pytest.mark.parametrize("mode", ["UBOOT", "FASTBOOT"])
+def test_lava_job_definition(mode, force_uart, shell_file, 
mock_collabora_farm, monkeypatch):
+    monkeypatch.setattr(lava.utils.lava_job_definition, "FORCE_UART", 
force_uart)
+
+    yaml = YAML()
+    yaml.default_flow_style = False
+
+    # Load the YAML output and check that it contains the expected keys and 
values
+    expected_job_dict = 
load_yaml_file(f"{mode}_force_uart={force_uart}_job_definition.yaml")
+
+    init_script_content = f"echo test {mode}"
+    job_submitter = job_submitter_factory(mode, 
shell_file(init_script_content))
+    job_definition = 
LAVAJobDefinition(job_submitter).generate_lava_job_definition()
+
+    job_dict = yaml.load(job_definition)
+
+    # Uncomment the following to update the expected YAML files
+    # yaml.dump(job_dict, 
Path(f"../../data/{mode}_force_uart={force_uart}_job_definition.yaml"))
+
+    # Check that the generated job definition matches the expected one
+    assert job_dict == expected_job_dict

Reply via email to