diff --git a/.github/workflows/ui_notebooks_test.yaml b/.github/workflows/ui_notebooks_test.yaml index 864330b9c..9dff9fafd 100644 --- a/.github/workflows/ui_notebooks_test.yaml +++ b/.github/workflows/ui_notebooks_test.yaml @@ -86,7 +86,8 @@ jobs: jq -r 'del(.cells[] | select(.source[] | contains("Create authentication object for user permissions")))' 3_widget_example.ipynb > 3_widget_example.ipynb.tmp && mv 3_widget_example.ipynb.tmp 3_widget_example.ipynb jq -r 'del(.cells[] | select(.source[] | contains("auth.logout()")))' 3_widget_example.ipynb > 3_widget_example.ipynb.tmp && mv 3_widget_example.ipynb.tmp 3_widget_example.ipynb # Set explicit namespace as SDK need it (currently) to resolve local queues - sed -i "s/head_memory_limits=2,/head_memory_limits=2, namespace='default',/" 3_widget_example.ipynb + sed -i "s/head_memory_limits=2,/head_memory_limits=2, namespace='default', image='quay.io/modh/ray:2.35.0-py39-cu121',/" 3_widget_example.ipynb + sed -i "s/view_clusters()/view_clusters('default')/" 3_widget_example.ipynb working-directory: demo-notebooks/guided-demos - name: Run UI notebook tests diff --git a/demo-notebooks/guided-demos/3_widget_example.ipynb b/demo-notebooks/guided-demos/3_widget_example.ipynb index 4d3d6ea70..c3bb6b862 100644 --- a/demo-notebooks/guided-demos/3_widget_example.ipynb +++ b/demo-notebooks/guided-demos/3_widget_example.ipynb @@ -19,7 +19,7 @@ "outputs": [], "source": [ "# Import pieces from codeflare-sdk\n", - "from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication" + "from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication, view_clusters" ] }, { @@ -61,7 +61,7 @@ "# Create and configure our cluster object\n", "# The SDK will try to find the name of your default local queue based on the annotation \"kueue.x-k8s.io/default-queue\": \"true\" unless you specify the local queue manually below\n", "cluster = Cluster(ClusterConfiguration(\n", - " name='raytest', \n", + " name='raytest',\n", " head_cpu_requests='500m',\n", " head_cpu_limits='500m',\n", " head_memory_requests=2,\n", @@ -73,12 +73,22 @@ " worker_cpu_limits=1,\n", " worker_memory_requests=2,\n", " worker_memory_limits=2,\n", - " # image=\"\", # Optional Field \n", - " write_to_file=False, # When enabled Ray Cluster yaml files are written to /HOME/.codeflare/resources \n", + " # image=\"\", # Optional Field\n", + " write_to_file=False, # When enabled Ray Cluster yaml files are written to /HOME/.codeflare/resources\n", " # local_queue=\"local-queue-name\" # Specify the local queue manually\n", "))" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3de6403c", + "metadata": {}, + "outputs": [], + "source": [ + "view_clusters()" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/codeflare_sdk/cluster/widgets.py b/src/codeflare_sdk/cluster/widgets.py index e68c52c42..ae2390fe1 100644 --- a/src/codeflare_sdk/cluster/widgets.py +++ b/src/codeflare_sdk/cluster/widgets.py @@ -253,7 +253,7 @@ def _delete_cluster( plural = "rayclusters" # Wait for the resource to be deleted - while True: + while timeout > 0: try: api_instance.get_namespaced_custom_object( group=group, @@ -321,17 +321,13 @@ def _fetch_cluster_data(namespace): } return pd.DataFrame(data) -# format_status takes a RayCluster status and applies colors and icons based on the status. + def _format_status(status): - if status == RayClusterStatus.READY: - return 'Ready ✓' - elif status == RayClusterStatus.SUSPENDED: - return 'Suspended ❄️' - elif status == RayClusterStatus.FAILED: - return 'Failed ✗' - elif status == RayClusterStatus.UNHEALTHY: - return 'Unhealthy' - elif status == RayClusterStatus.UNKNOWN: - return 'Unknown' - else: - return status + status_map = { + RayClusterStatus.READY: 'Ready ✓', + RayClusterStatus.SUSPENDED: 'Suspended ❄️', + RayClusterStatus.FAILED: 'Failed ✗', + RayClusterStatus.UNHEALTHY: 'Unhealthy', + RayClusterStatus.UNKNOWN: 'Unknown' + } + return status_map.get(status, status) diff --git a/tests/unit_test.py b/tests/unit_test.py index c3f59f63c..b9682996a 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -77,16 +77,7 @@ is_openshift_cluster, ) -from codeflare_sdk.cluster.widgets import ( - cluster_up_down_buttons, - view_clusters, - _on_cluster_click, - _on_delete_button_click, - _on_list_jobs_button_click, - _on_ray_dashboard_button_click, - _format_status, - _fetch_cluster_data, -) +import codeflare_sdk.cluster.widgets as cf_widgets import pandas as pd import openshift @@ -2933,7 +2924,7 @@ def test_cluster_up_down_buttons(mocker): MockButton.side_effect = [mock_up_button, mock_down_button] # Call the method under test - cluster_up_down_buttons(cluster) + cf_widgets.cluster_up_down_buttons(cluster) # Simulate checkbox being checked or unchecked mock_wait_ready_check_box.value = True # Simulate checkbox being checked @@ -2975,7 +2966,7 @@ def test_view_clusters(mocker): # Return empty dataframe when no clusters are found mocker.patch("codeflare_sdk.cluster.cluster.list_all_clusters", return_value=[]) - df = _fetch_cluster_data(namespace="default") + df = cf_widgets._fetch_cluster_data(namespace="default") assert df.empty test_df=pd.DataFrame({ @@ -3022,33 +3013,33 @@ def test_view_clusters(mocker): MockOutput.return_value = mock_output # Call the function under test - view_clusters(namespace="default") + cf_widgets.view_clusters(namespace="default") # Simulate selecting a cluster mock_toggle.value = "test-cluster" selection_change = {"new": "test-cluster"} - _on_cluster_click(selection_change, mock_output, "default", mock_toggle) + cf_widgets._on_cluster_click(selection_change, mock_output, "default", mock_toggle) # Assert that the toggle options are set correctly mock_toggle.observe.assert_called() # Simulate clicking the list jobs button - _on_list_jobs_button_click(None, mock_toggle, test_df, mock_output, mock_output) + cf_widgets._on_list_jobs_button_click(None, mock_toggle, test_df, mock_output, mock_output) mock_javascript.assert_called() # Simulate clicking the Ray dashboard button - _on_ray_dashboard_button_click(None, mock_toggle, test_df, mock_output, mock_output) + cf_widgets._on_ray_dashboard_button_click(None, mock_toggle, test_df, mock_output, mock_output) mock_javascript.assert_called() # Simulate clicking the delete button - _on_delete_button_click(None, mock_toggle, test_df, mock_output, mock_output, + cf_widgets._on_delete_button_click(None, mock_toggle, test_df, mock_output, mock_output, mock_delete_button, mock_list_jobs_button, mock_ray_dashboard_button) def test_fetch_cluster_data(mocker): # Return empty dataframe when no clusters are found mocker.patch("codeflare_sdk.cluster.cluster.list_all_clusters", return_value=[]) - df = _fetch_cluster_data(namespace="default") + df = cf_widgets._fetch_cluster_data(namespace="default") assert df.empty # Create mock RayCluster objects @@ -3088,7 +3079,7 @@ def test_fetch_cluster_data(mocker): with patch('codeflare_sdk.cluster.cluster.list_all_clusters', return_value=[mock_raycluster1, mock_raycluster2]): # Call the function under test - df = _fetch_cluster_data(namespace='default') + df = cf_widgets._fetch_cluster_data(namespace='default') # Expected DataFrame expected_data = { @@ -3123,11 +3114,11 @@ def test_format_status(): ] for status, expected_output in test_cases: - assert _format_status(status) == expected_output, f"Failed for status: {status}" + assert cf_widgets._format_status(status) == expected_output, f"Failed for status: {status}" # Test an unrecognized status unrecognized_status = 'NotAStatus' - assert _format_status(unrecognized_status) == 'NotAStatus', "Failed for unrecognized status" + assert cf_widgets._format_status(unrecognized_status) == 'NotAStatus', "Failed for unrecognized status" # Make sure to always keep this function last diff --git a/ui-tests/tests/widget_notebook_example.test.ts b/ui-tests/tests/widget_notebook_example.test.ts index 798c2eb60..95e84d66f 100644 --- a/ui-tests/tests/widget_notebook_example.test.ts +++ b/ui-tests/tests/widget_notebook_example.test.ts @@ -30,11 +30,13 @@ test.describe("Visual Regression", () => { tmpPath, }) => { const notebook = "3_widget_example.ipynb"; + const namespace = 'default'; await page.notebook.openByPath(`${tmpPath}/${notebook}`); await page.notebook.activate(notebook); const captures: (Buffer | null)[] = []; // Array to store cell screenshots const cellCount = await page.notebook.getCellCount(); + console.log(`Cell count: ${cellCount}`); // Run all cells and capture their screenshots await page.notebook.runCellByCell({ @@ -59,25 +61,27 @@ test.describe("Visual Regression", () => { } } - const widgetCellIndex = 3; + // At this point, all cells have been ran, and their screenshots have been captured. + // We now interact with the widgets in the notebook. + const upDownWidgetCellIndex = 3; // 4 on OpenShift - await waitForWidget(page, widgetCellIndex, 'input[type="checkbox"]'); - await waitForWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")'); - await waitForWidget(page, widgetCellIndex, 'button:has-text("Cluster Up")'); + await waitForWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]'); + await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")'); + await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")'); - await interactWithWidget(page, widgetCellIndex, 'input[type="checkbox"]', async (checkbox) => { + await interactWithWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => { await checkbox.click(); const isChecked = await checkbox.isChecked(); expect(isChecked).toBe(true); }); - await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { + await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { await button.click(); const clusterDownMessage = await page.waitForSelector('text=No instances found, nothing to be done.', { timeout: 5000 }); expect(clusterDownMessage).not.toBeNull(); }); - await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Up")', async (button) => { + await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => { await button.click(); const successMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been created', { timeout: 10000 }); @@ -95,13 +99,51 @@ test.describe("Visual Regression", () => { await runPreviousCell(page, cellCount, '(, True)'); - await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { + await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { await button.click(); const clusterDownMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been deleted', { timeout: 5000 }); expect(clusterDownMessage).not.toBeNull(); }); await runPreviousCell(page, cellCount, '(, False)'); + + // view_clusters table with buttons + await interactWithWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => { + await checkbox.click(); + const isChecked = await checkbox.isChecked(); + expect(isChecked).toBe(false); + }); + + await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => { + await button.click(); + const successMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been created', { timeout: 10000 }); + expect(successMessage).not.toBeNull(); + }); + + const viewClustersCellIndex = 4; // 5 on OpenShift + await page.notebook.runCell(cellCount - 2, true); + await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("Open Ray Dashboard")', async (button) => { + await button.click(); + const successMessage = await page.waitForSelector('text=Opening Ray Dashboard for raytest cluster', { timeout: 5000 }); + expect(successMessage).not.toBeNull(); + }); + + await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("View Jobs")', async (button) => { + await button.click(); + const successMessage = await page.waitForSelector('text=Opening Ray Jobs Dashboard for raytest cluster', { timeout: 5000 }); + expect(successMessage).not.toBeNull(); + }); + + await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("Delete Cluster")', async (button) => { + await button.click(); + + const noClustersMessage = await page.waitForSelector(`text=No clusters found in the ${namespace} namespace.`, { timeout: 5000 }); + expect(noClustersMessage).not.toBeNull(); + const successMessage = await page.waitForSelector(`text=Cluster raytest in the ${namespace} namespace was deleted successfully.`, { timeout: 5000 }); + expect(successMessage).not.toBeNull(); + }); + + await runPreviousCell(page, cellCount, '(, False)'); }); });