Skip to content

Commit

Permalink
Add UI table to regression and functionality tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristianZaccaria committed Sep 25, 2024
1 parent a8ba6f7 commit d4ce271
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 48 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ui_notebooks_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions demo-notebooks/guided-demos/3_widget_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down
24 changes: 10 additions & 14 deletions src/codeflare_sdk/cluster/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 '<span style="color: green;">Ready ✓</span>'
elif status == RayClusterStatus.SUSPENDED:
return '<span style="color: #007BFF;">Suspended ❄️</span>'
elif status == RayClusterStatus.FAILED:
return '<span style="color: red;">Failed ✗</span>'
elif status == RayClusterStatus.UNHEALTHY:
return '<span style="color: purple;">Unhealthy</span>'
elif status == RayClusterStatus.UNKNOWN:
return '<span style="color: purple;">Unknown</span>'
else:
return status
status_map = {
RayClusterStatus.READY: '<span style="color: green;">Ready ✓</span>',
RayClusterStatus.SUSPENDED: '<span style="color: #007BFF;">Suspended ❄️</span>',
RayClusterStatus.FAILED: '<span style="color: red;">Failed ✗</span>',
RayClusterStatus.UNHEALTHY: '<span style="color: purple;">Unhealthy</span>',
RayClusterStatus.UNKNOWN: '<span style="color: purple;">Unknown</span>'
}
return status_map.get(status, status)
33 changes: 12 additions & 21 deletions tests/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
58 changes: 50 additions & 8 deletions ui-tests/tests/widget_notebook_example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 });
Expand All @@ -95,13 +99,51 @@ test.describe("Visual Regression", () => {

await runPreviousCell(page, cellCount, '(<CodeFlareClusterStatus.READY: 1>, 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, '(<CodeFlareClusterStatus.UNKNOWN: 6>, 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, '(<CodeFlareClusterStatus.UNKNOWN: 6>, False)');
});
});

Expand Down

0 comments on commit d4ce271

Please sign in to comment.