From d31d7059dab939e34ff6f6e55a9d30c91b6f1da7 Mon Sep 17 00:00:00 2001 From: ArturoAmorQ Date: Thu, 20 Nov 2025 16:42:49 +0100 Subject: [PATCH 1/2] Add script to test jupyterlite notebooks --- _output/jupyterlite_pytest.py | 337 ++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 _output/jupyterlite_pytest.py diff --git a/_output/jupyterlite_pytest.py b/_output/jupyterlite_pytest.py new file mode 100644 index 000000000..5c67311ea --- /dev/null +++ b/_output/jupyterlite_pytest.py @@ -0,0 +1,337 @@ +import csv +import os +import pytest +import time +from playwright.sync_api import Page, TimeoutError + +NOTEBOOK_NAMES = [ + "01_tabular_data_exploration.ipynb", + "01_tabular_data_exploration_ex_01.ipynb", + "01_tabular_data_exploration_sol_01.ipynb", + "02_numerical_pipeline_cross_validation.ipynb", + "02_numerical_pipeline_ex_00.ipynb", + "02_numerical_pipeline_ex_01.ipynb", + "02_numerical_pipeline_hands_on.ipynb", + "02_numerical_pipeline_introduction.ipynb", + "02_numerical_pipeline_scaling.ipynb", + "02_numerical_pipeline_sol_00.ipynb", + "02_numerical_pipeline_sol_01.ipynb", + "03_categorical_pipeline.ipynb", + "03_categorical_pipeline_column_transformer.ipynb", + "03_categorical_pipeline_ex_01.ipynb", + "03_categorical_pipeline_ex_02.ipynb", + "03_categorical_pipeline_sol_01.ipynb", + "03_categorical_pipeline_sol_02.ipynb", + "03_categorical_pipeline_visualization.ipynb", + "clustering_ex_01.ipynb", + "clustering_ex_02.ipynb", + "clustering_hdbscan.ipynb", + "clustering_kmeans.ipynb", + "clustering_kmeans_ex_01.ipynb", + "clustering_kmeans_sol_01.ipynb", + "clustering_kmeans_sol_02.ipynb", + "clustering_sol_01.ipynb", + "clustering_sol_02.ipynb", + "clustering_supervised_metrics.ipynb", + "clustering_transformer.ipynb", + "cross_validation_baseline.ipynb", + "cross_validation_ex_01.ipynb", + "cross_validation_ex_02.ipynb", + "cross_validation_grouping.ipynb", + "cross_validation_learning_curve.ipynb", + "cross_validation_nested.ipynb", + "cross_validation_sol_01.ipynb", + "cross_validation_sol_02.ipynb", + "cross_validation_stratification.ipynb", + "cross_validation_time.ipynb", + "cross_validation_train_test.ipynb", + "cross_validation_validation_curve.ipynb", + "ensemble_adaboost.ipynb", + "ensemble_bagging.ipynb", + "ensemble_ex_01.ipynb", + "ensemble_ex_02.ipynb", + "ensemble_ex_03.ipynb", + "ensemble_ex_04.ipynb", + "ensemble_gradient_boosting.ipynb", + "ensemble_hist_gradient_boosting.ipynb", + "ensemble_hyperparameters.ipynb", + "ensemble_introduction.ipynb", + "ensemble_random_forest.ipynb", + "ensemble_sol_01.ipynb", + "ensemble_sol_02.ipynb", + "ensemble_sol_03.ipynb", + "ensemble_sol_04.ipynb", + "feature_selection_ex_01.ipynb", + "feature_selection_introduction.ipynb", + "feature_selection_limitation_model.ipynb", + "feature_selection_sol_01.ipynb", + "linear_models_ex_01.ipynb", + "linear_models_ex_02.ipynb", + "linear_models_ex_03.ipynb", + "linear_models_ex_04.ipynb", + "linear_models_feature_engineering_classification.ipynb", + "linear_models_regularization.ipynb", + "linear_models_sol_01.ipynb", + "linear_models_sol_02.ipynb", + "linear_models_sol_03.ipynb", + "linear_models_sol_04.ipynb", + "linear_regression_in_sklearn.ipynb", + "linear_regression_non_linear_link.ipynb", + "linear_regression_without_sklearn.ipynb", + "logistic_regression.ipynb", + "metrics_classification.ipynb", + "metrics_ex_01.ipynb", + "metrics_ex_02.ipynb", + "metrics_regression.ipynb", + "metrics_sol_01.ipynb", + "metrics_sol_02.ipynb", + "parameter_tuning_ex_02.ipynb", + "parameter_tuning_ex_03.ipynb", + "parameter_tuning_grid_search.ipynb", + "parameter_tuning_manual.ipynb", + "parameter_tuning_nested.ipynb", + "parameter_tuning_parallel_plot.ipynb", + "parameter_tuning_randomized_search.ipynb", + "parameter_tuning_sol_02.ipynb", + "parameter_tuning_sol_03.ipynb", + "trees_classification.ipynb", + "trees_dataset.ipynb", + "trees_ex_01.ipynb", + "trees_ex_02.ipynb", + "trees_hyperparameters.ipynb", + "trees_regression.ipynb", + "trees_sol_01.ipynb", + "trees_sol_02.ipynb", +] + + +def log_to_csv(notebook_name, execution_time, has_errors, has_warnings): + """Log test results to CSV file.""" + csv_file = "test_results.csv" + file_exists = os.path.isfile(csv_file) + + with open(csv_file, "a", newline="") as f: + fieldnames = [ + "notebook_name", + "execution_time", + "has_errors", + "has_warnings", + ] + writer = csv.DictWriter(f, fieldnames=fieldnames) + + # Write header if file is new + if not file_exists: + writer.writeheader() + + # Write the test result + writer.writerow( + { + "notebook_name": notebook_name, + "execution_time": f"{execution_time:.2f}", + "has_errors": has_errors, + "has_warnings": has_warnings, + } + ) + + +@pytest.fixture(scope="function") +def browser_context_args(): + """Configure browser context with appropriate settings.""" + return { + "viewport": {"width": 1280, "height": 720}, + "ignore_https_errors": True, + } + + +@pytest.mark.parametrize("notebook_name", NOTEBOOK_NAMES) +def test_run_jupyterlite_notebook(page: Page, notebook_name: str): + """ + Test that runs a JupyterLite notebook using Playwright. + + Steps: + 1. Navigate to the notebook URL + 2. Wait for kernel to initialize + 3. Click on Run menu + 4. Select "Run all cells" + """ + + # Construct URL with the notebook name + base_url = ( + "https://probabl-static.france-ioi.org/jupyter/v0/notebooks/index.html" + ) + url = f"{base_url}?sLocale=en&path=notebooks%2F{notebook_name}" + print(f"\nTesting notebook: {notebook_name}") + + # Navigate to the notebook + page.goto(url, wait_until="networkidle") + + # Wait for the main notebook interface to be visible + page.wait_for_selector(".jp-Notebook", state="visible", timeout=30000) + + # Wait for kernel to initialize + kernel_ready = False + max_wait_time = 60 # Maximum 60 seconds to wait for kernel + start_time = time.time() + + while not kernel_ready and (time.time() - start_time) < max_wait_time: + try: + # Check for kernel status - looking for "Idle" or ready state + # JupyterLite may show this in different ways + kernel_status = page.locator('[data-status="idle"]').first + if kernel_status.is_visible(): + kernel_ready = True + break + + # # Alternative: check for the kernel indicator in the toolbar + # toolbar_kernel = page.locator(".jp-KernelStatus-fade").first + # if toolbar_kernel.is_visible(): + # kernel_ready = True + # break + + # # Also check if we can see the kernel name in the toolbar + # kernel_name = page.locator(".jp-KernelName").first + # if kernel_name.is_visible(): + # # Give it a bit more time to fully initialize + # page.wait_for_timeout(2000) + # kernel_ready = True + # break + + except (TimeoutError, AttributeError): + pass + + # Wait a bit before checking again + page.wait_for_timeout(1000) + + if not kernel_ready: + print("Warning: Kernel initialization timeout, proceeding anyway...") + + # Additional wait to ensure everything is loaded + page.wait_for_timeout(2000) + + # Click on the "Run" menu + run_menu = page.locator('div.lm-MenuBar-itemLabel:has-text("Run")').first + run_menu.click() + + # Wait for menu to open + page.wait_for_timeout(500) + + # Click on "Run All Cells" option + run_all_cells = page.locator( + 'div.lm-Menu-itemLabel:has-text("Run All Cells")' + ).first + run_all_cells.click() + execution_start_time = time.time() + + # Wait for cells to start executing + page.wait_for_timeout(2000) + + # Get all code cells in the notebook + code_cells = page.locator(".jp-CodeCell") + total_code_cells = code_cells.count() + + # Monitor cell execution + max_execution_time = 300 # 5 minutes max for all cells to execute + last_executed_count = 0 + stable_count = 0 + start_time = time.time() + all_cells_complete = False + + while ( + not all_cells_complete + and (time.time() - start_time) < max_execution_time + ): + try: + # Count executed cells - those with [N] where N is a number + executed_prompts = page.locator(".jp-InputPrompt").all() + executed_cells = 0 + running_cells = 0 + + for prompt in executed_prompts: + text = prompt.inner_text().strip() + if "[*]" in text: + running_cells += 1 + elif text and text.startswith("[") and text.endswith("]"): + # It's [1], [2], etc. + executed_cells += 1 + + # Check if execution count is stable + if executed_cells == last_executed_count: + stable_count += 1 + else: + stable_count = 0 + last_executed_count = executed_cells + + # Consider complete if all expected cells executed or stable state + if executed_cells >= total_code_cells: + all_cells_complete = True + execution_time = time.time() - execution_start_time + print( + f"All {executed_cells} cells completed in" + f" {execution_time:.2f} seconds!" + ) + break + elif running_cells == 0 and stable_count >= 3: + all_cells_complete = True + execution_time = time.time() - execution_start_time + print( + f"Execution complete: {total_code_cells} cells in" + f" {execution_time:.2f} seconds" + ) + break + + # Adaptive waiting + if running_cells > 0: + page.wait_for_timeout(1000) + else: + page.wait_for_timeout(3000) + + except Exception as e: + print(f"Error checking cell status: {e}") + page.wait_for_timeout(2000) + + if not all_cells_complete: + print( + "Warning: Cell execution timeout after" + f" {max_execution_time} seconds" + ) + # Optionally take a debug screenshot + # page.screenshot(path="execution_timeout.png") + # print("Timeout screenshot saved as execution_timeout.png") + else: + execution_time = time.time() - execution_start_time + + # Final wait to ensure everything has settled + page.wait_for_timeout(3000) + + # Verify that cells have output + output_areas = page.locator(".jp-OutputArea") + output_count = output_areas.count() + print(f"Found {output_count} output areas") + + # Verify the notebook is still responsive and cells have executed + assert page.locator( + ".jp-Notebook" + ).is_visible(), "Notebook interface is not visible" + assert ( + output_count > 0 or total_code_cells > 0 + ), "No cells or outputs found" + + # Check for any error and warning outputs + error_outputs = page.locator( + ".jp-OutputArea-output[data-mime-type*='error']" + ).count() + warning_outputs = page.locator( + ".jp-OutputArea-output:has-text('Warning')" + ).count() + has_errors = 1 if error_outputs > 0 else 0 + has_warnings = 1 if warning_outputs > 0 else 0 + if has_errors: + pytest.fail("Notebook execution failed with errors") + elif has_warnings: + print("⚠️ Notebook has warnings") + + log_to_csv(notebook_name, execution_time, has_errors, has_warnings) + print( + f"Total execution time for {notebook_name}:" + f" {execution_time:.2f} seconds" + ) From 1af6902b6f0fc711b7649240d2715068f370c632 Mon Sep 17 00:00:00 2001 From: ArturoAmorQ Date: Fri, 21 Nov 2025 11:54:38 +0100 Subject: [PATCH 2/2] Add csv file --- _output/test_results.csv | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 _output/test_results.csv diff --git a/_output/test_results.csv b/_output/test_results.csv new file mode 100644 index 000000000..190499848 --- /dev/null +++ b/_output/test_results.csv @@ -0,0 +1,83 @@ +notebook_name,execution_time,has_errors,has_warnings +01_tabular_data_exploration.ipynb,19.48,0,0 +01_tabular_data_exploration_ex_01.ipynb,5.36,0,0 +01_tabular_data_exploration_sol_01.ipynb,9.49,0,0 +02_numerical_pipeline_cross_validation.ipynb,13.50,0,0 +02_numerical_pipeline_ex_00.ipynb,11.83,0,1 +02_numerical_pipeline_ex_01.ipynb,13.87,0,0 +02_numerical_pipeline_hands_on.ipynb,12.66,0,0 +02_numerical_pipeline_introduction.ipynb,18.62,0,0 +02_numerical_pipeline_scaling.ipynb,18.81,0,0 +02_numerical_pipeline_sol_00.ipynb,17.90,0,1 +02_numerical_pipeline_sol_01.ipynb,12.90,0,0 +03_categorical_pipeline.ipynb,15.91,0,0 +03_categorical_pipeline_column_transformer.ipynb,17.97,0,0 +03_categorical_pipeline_ex_01.ipynb,12.56,0,0 +03_categorical_pipeline_ex_02.ipynb,19.73,0,0 +03_categorical_pipeline_sol_01.ipynb,17.65,0,0 +03_categorical_pipeline_sol_02.ipynb,46.44,0,0 +03_categorical_pipeline_visualization.ipynb,12.42,0,0 +clustering_ex_01.ipynb,22.05,0,0 +clustering_ex_02.ipynb,7.39,0,0 +clustering_hdbscan.ipynb,30.30,0,0 +clustering_kmeans.ipynb,36.38,0,0 +cross_validation_baseline.ipynb,16.34,0,0 +cross_validation_ex_01.ipynb,6.14,0,0 +cross_validation_ex_02.ipynb,4.15,0,0 +cross_validation_grouping.ipynb,12.32,0,0 +cross_validation_learning_curve.ipynb,8.12,0,0 +cross_validation_nested.ipynb,46.24,0,0 +cross_validation_sol_01.ipynb,8.17,0,0 +cross_validation_sol_02.ipynb,13.28,0,0 +cross_validation_stratification.ipynb,18.69,0,0 +cross_validation_time.ipynb,17.68,0,0 +cross_validation_train_test.ipynb,30.31,0,0 +cross_validation_validation_curve.ipynb,8.13,0,0 +ensemble_adaboost.ipynb,18.76,0,1 +ensemble_bagging.ipynb,21.52,0,0 +ensemble_ex_01.ipynb,11.47,0,0 +ensemble_ex_02.ipynb,11.70,0,0 +ensemble_ex_03.ipynb,11.68,0,0 +ensemble_ex_04.ipynb,11.76,0,0 +ensemble_gradient_boosting.ipynb,250.92,0,0 +ensemble_hist_gradient_boosting.ipynb,93.91,0,1 +ensemble_hyperparameters.ipynb,146.77,0,0 +ensemble_introduction.ipynb,50.00,0,0 +ensemble_random_forest.ipynb,33.78,0,0 +ensemble_sol_01.ipynb,63.21,0,0 +ensemble_sol_02.ipynb,31.25,0,0 +ensemble_sol_03.ipynb,24.11,0,0 +ensemble_sol_04.ipynb,90.48,0,0 +linear_models_ex_02.ipynb,6.58,0,0 +linear_models_ex_03.ipynb,9.96,0,0 +linear_models_ex_04.ipynb,147.63,0,0 +linear_models_sol_02.ipynb,15.39,0,0 +linear_models_sol_03.ipynb,31.55,0,0 +linear_models_sol_04.ipynb,18.31,0,0 +linear_regression_in_sklearn.ipynb,17.59,0,0 +linear_regression_non_linear_link.ipynb,14.44,0,0 +linear_regression_without_sklearn.ipynb,9.27,0,0 +logistic_regression.ipynb,23.55,0,0 +metrics_classification.ipynb,14.31,0,0 +metrics_ex_01.ipynb,12.45,0,0 +metrics_ex_02.ipynb,4.13,0,0 +metrics_regression.ipynb,12.29,0,0 +metrics_sol_01.ipynb,19.70,0,0 +metrics_sol_02.ipynb,19.41,0,0 +parameter_tuning_ex_02.ipynb,10.33,0,0 +parameter_tuning_ex_03.ipynb,11.33,0,0 +parameter_tuning_grid_search.ipynb,24.53,0,0 +parameter_tuning_manual.ipynb,16.58,0,0 +parameter_tuning_nested.ipynb,45.96,0,0 +parameter_tuning_parallel_plot.ipynb,15.79,0,0 +parameter_tuning_randomized_search.ipynb,40.26,0,0 +parameter_tuning_sol_02.ipynb,15.37,0,0 +parameter_tuning_sol_03.ipynb,164.00,0,0 +trees_classification.ipynb,20.80,0,0 +trees_dataset.ipynb,9.31,0,0 +trees_ex_01.ipynb,11.52,0,0 +trees_ex_02.ipynb,16.15,0,0 +trees_hyperparameters.ipynb,28.19,0,0 +trees_regression.ipynb,15.54,0,0 +trees_sol_01.ipynb,16.28,0,0 +trees_sol_02.ipynb,15.46,0,0