|
1 | 1 | # Copyright OpenSearch Contributors |
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
| 4 | +import argparse |
4 | 5 | import doctest |
| 6 | +import json |
5 | 7 | import os |
6 | 8 | import os.path |
7 | | -import zc.customdoctests |
8 | | -import json |
9 | | -import re |
10 | 9 | import random |
| 10 | +import re |
11 | 11 | import subprocess |
| 12 | +import sys |
12 | 13 | import unittest |
13 | | -import click |
14 | | - |
15 | 14 | from functools import partial |
| 15 | + |
| 16 | +import click |
| 17 | +import zc.customdoctests |
| 18 | +from opensearch_sql_cli.formatter import Formatter |
16 | 19 | from opensearch_sql_cli.opensearch_connection import OpenSearchConnection |
17 | 20 | from opensearch_sql_cli.utils import OutputSettings |
18 | | -from opensearch_sql_cli.formatter import Formatter |
19 | 21 | from opensearchpy import OpenSearch, helpers |
20 | 22 |
|
21 | 23 | ENDPOINT = "http://localhost:9200" |
@@ -244,3 +246,292 @@ def load_tests(loader, suite, ignore): |
244 | 246 | random.shuffle(tests) |
245 | 247 |
|
246 | 248 | return DocTests(tests) |
| 249 | + |
| 250 | + |
| 251 | +# Single file doctest functionality |
| 252 | +def find_doc_file(filename_or_path): |
| 253 | + """Find documentation file by name or return the path if it's already a full path""" |
| 254 | + # If it's already a full path that exists, return it |
| 255 | + if os.path.exists(filename_or_path): |
| 256 | + return filename_or_path |
| 257 | + |
| 258 | + # If it's just a filename, search for it in the docs directory |
| 259 | + if not os.path.sep in filename_or_path: |
| 260 | + try: |
| 261 | + with open('../docs/category.json') as json_file: |
| 262 | + category = json.load(json_file) |
| 263 | + |
| 264 | + # Search in all categories |
| 265 | + all_docs = category['bash'] + category['ppl_cli'] + category['sql_cli'] |
| 266 | + |
| 267 | + # Add .rst extension if not present |
| 268 | + search_filename = filename_or_path |
| 269 | + if not search_filename.endswith('.rst'): |
| 270 | + search_filename += '.rst' |
| 271 | + |
| 272 | + # Find files that end with the given filename |
| 273 | + matches = [doc for doc in all_docs if doc.endswith(search_filename)] |
| 274 | + |
| 275 | + if len(matches) == 1: |
| 276 | + found_path = f"../docs/{matches[0]}" |
| 277 | + print(f"Found: {found_path}") |
| 278 | + return found_path |
| 279 | + elif len(matches) > 1: |
| 280 | + print(f"Multiple files found matching '{search_filename}':") |
| 281 | + for match in matches: |
| 282 | + print(f" ../docs/{match}") |
| 283 | + print("Please specify the full path or a more specific filename.") |
| 284 | + return None |
| 285 | + else: |
| 286 | + print(f"No documentation file found matching '{search_filename}'") |
| 287 | + print("Use --list to see all available files") |
| 288 | + return None |
| 289 | + |
| 290 | + except Exception as e: |
| 291 | + print(f"Error searching for file: {e}") |
| 292 | + return None |
| 293 | + |
| 294 | + # If it's a relative path, try to find it |
| 295 | + if not filename_or_path.startswith('../docs/'): |
| 296 | + potential_path = f"../docs/{filename_or_path}" |
| 297 | + if os.path.exists(potential_path): |
| 298 | + return potential_path |
| 299 | + |
| 300 | + return filename_or_path |
| 301 | + |
| 302 | + |
| 303 | +def determine_doc_type(file_path): |
| 304 | + """Determine the type of documentation file based on category.json""" |
| 305 | + try: |
| 306 | + with open('../docs/category.json') as json_file: |
| 307 | + category = json.load(json_file) |
| 308 | + |
| 309 | + # Convert absolute path to relative path from docs directory |
| 310 | + rel_path = os.path.relpath(file_path, '../docs') |
| 311 | + |
| 312 | + if rel_path in category['bash']: |
| 313 | + return 'bash' |
| 314 | + elif rel_path in category['ppl_cli']: |
| 315 | + return 'ppl_cli' |
| 316 | + elif rel_path in category['sql_cli']: |
| 317 | + return 'sql_cli' |
| 318 | + else: |
| 319 | + # Try to guess based on file path |
| 320 | + if '/ppl/' in file_path: |
| 321 | + return 'ppl_cli' |
| 322 | + elif '/sql/' in file_path or '/dql/' in file_path: |
| 323 | + return 'sql_cli' |
| 324 | + else: |
| 325 | + return 'bash' # default fallback |
| 326 | + except Exception as e: |
| 327 | + print(f"Warning: Could not determine doc type from category.json: {e}") |
| 328 | + # Fallback to path-based detection |
| 329 | + if '/ppl/' in file_path: |
| 330 | + return 'ppl_cli' |
| 331 | + elif '/sql/' in file_path or '/dql/' in file_path: |
| 332 | + return 'sql_cli' |
| 333 | + else: |
| 334 | + return 'bash' |
| 335 | + |
| 336 | + |
| 337 | +def run_single_doctest(file_path, verbose=False, endpoint=None): |
| 338 | + """Run doctest for a single documentation file""" |
| 339 | + |
| 340 | + if not os.path.exists(file_path): |
| 341 | + print(f"Error: File {file_path} does not exist") |
| 342 | + return False |
| 343 | + |
| 344 | + # Update endpoint if provided |
| 345 | + if endpoint: |
| 346 | + global ENDPOINT |
| 347 | + ENDPOINT = endpoint |
| 348 | + print(f"Using custom endpoint: {endpoint}") |
| 349 | + |
| 350 | + doc_type = determine_doc_type(file_path) |
| 351 | + print(f"Detected doc type: {doc_type}") |
| 352 | + print(f"Running doctest for: {file_path}") |
| 353 | + |
| 354 | + # Configure doctest options |
| 355 | + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS |
| 356 | + if verbose: |
| 357 | + optionflags |= doctest.REPORT_NDIFF |
| 358 | + |
| 359 | + # Choose appropriate parser and setup based on doc type |
| 360 | + if doc_type == 'bash': |
| 361 | + parser = bash_parser |
| 362 | + setup_func = set_up_test_indices |
| 363 | + globs = { |
| 364 | + 'sh': partial( |
| 365 | + subprocess.run, |
| 366 | + stdin=subprocess.PIPE, |
| 367 | + stdout=subprocess.PIPE, |
| 368 | + stderr=subprocess.STDOUT, |
| 369 | + timeout=60, |
| 370 | + shell=True |
| 371 | + ), |
| 372 | + 'pretty_print': pretty_print |
| 373 | + } |
| 374 | + elif doc_type == 'ppl_cli': |
| 375 | + parser = ppl_cli_parser |
| 376 | + setup_func = set_up_test_indices |
| 377 | + globs = {} |
| 378 | + else: # sql_cli |
| 379 | + parser = sql_cli_parser |
| 380 | + setup_func = set_up_test_indices |
| 381 | + globs = {} |
| 382 | + |
| 383 | + try: |
| 384 | + print("Setting up test environment...") |
| 385 | + |
| 386 | + # Create and run the doctest suite |
| 387 | + suite = doctest.DocFileSuite( |
| 388 | + file_path, |
| 389 | + parser=parser, |
| 390 | + setUp=setup_func, |
| 391 | + tearDown=tear_down, |
| 392 | + optionflags=optionflags, |
| 393 | + encoding='utf-8', |
| 394 | + globs=globs |
| 395 | + ) |
| 396 | + |
| 397 | + # Run the test |
| 398 | + runner = unittest.TextTestRunner(verbosity=2 if verbose else 1) |
| 399 | + result = runner.run(suite) |
| 400 | + |
| 401 | + # Print summary |
| 402 | + if result.wasSuccessful(): |
| 403 | + print(f"\nSUCCESS: All tests in {os.path.basename(file_path)} passed!") |
| 404 | + print(f"Tests run: {result.testsRun}, Failures: {len(result.failures)}, Errors: {len(result.errors)}") |
| 405 | + return True |
| 406 | + else: |
| 407 | + print(f"\nFAILED: {len(result.failures + result.errors)} test(s) failed in {os.path.basename(file_path)}") |
| 408 | + print(f"Tests run: {result.testsRun}, Failures: {len(result.failures)}, Errors: {len(result.errors)}") |
| 409 | + |
| 410 | + if verbose: |
| 411 | + print("\nDetailed failure information:") |
| 412 | + for failure in result.failures: |
| 413 | + print(f"\n--- FAILURE in {failure[0]} ---") |
| 414 | + print(failure[1]) |
| 415 | + for error in result.errors: |
| 416 | + print(f"\n--- ERROR in {error[0]} ---") |
| 417 | + print(error[1]) |
| 418 | + else: |
| 419 | + print("Use --verbose for detailed failure information") |
| 420 | + |
| 421 | + return False |
| 422 | + |
| 423 | + except Exception as e: |
| 424 | + print(f"Error running doctest: {e}") |
| 425 | + if verbose: |
| 426 | + import traceback |
| 427 | + traceback.print_exc() |
| 428 | + return False |
| 429 | + |
| 430 | + |
| 431 | +def list_available_docs(): |
| 432 | + """List all available documentation files that can be tested""" |
| 433 | + try: |
| 434 | + with open('../docs/category.json') as json_file: |
| 435 | + category = json.load(json_file) |
| 436 | + |
| 437 | + print("Available documentation files for testing:") |
| 438 | + print(f"\nBash-based docs ({len(category['bash'])} files):") |
| 439 | + for doc in sorted(category['bash']): |
| 440 | + print(f" ../docs/{doc}") |
| 441 | + |
| 442 | + print(f"\nPPL CLI docs ({len(category['ppl_cli'])} files):") |
| 443 | + for doc in sorted(category['ppl_cli']): |
| 444 | + print(f" ../docs/{doc}") |
| 445 | + |
| 446 | + print(f"\nSQL CLI docs ({len(category['sql_cli'])} files):") |
| 447 | + for doc in sorted(category['sql_cli']): |
| 448 | + print(f" ../docs/{doc}") |
| 449 | + |
| 450 | + total_docs = len(category['bash']) + len(category['ppl_cli']) + len(category['sql_cli']) |
| 451 | + print(f"\nTotal: {total_docs} documentation files available for testing") |
| 452 | + |
| 453 | + except Exception as e: |
| 454 | + print(f"Error reading category.json: {e}") |
| 455 | + |
| 456 | + |
| 457 | +def main(): |
| 458 | + """Main entry point for single file testing""" |
| 459 | + parser = argparse.ArgumentParser( |
| 460 | + description="Run doctest for one or more documentation files, or all files if no arguments provided", |
| 461 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 462 | + epilog=""" |
| 463 | +Examples: |
| 464 | + python test_docs.py # Run all tests (default behavior) |
| 465 | + python test_docs.py stats # Run single file (extension optional) |
| 466 | + python test_docs.py stats.rst --verbose |
| 467 | + python test_docs.py stats fields basics |
| 468 | + python test_docs.py ../docs/user/ppl/cmd/stats.rst --endpoint http://localhost:9201 |
| 469 | +
|
| 470 | +Performance Tips: |
| 471 | + - Use --verbose for detailed debugging information |
| 472 | + - Ensure OpenSearch is running on the specified endpoint before testing |
| 473 | + - Extension .rst can be omitted for convenience |
| 474 | + """ |
| 475 | + ) |
| 476 | + |
| 477 | + parser.add_argument('file_paths', nargs='*', help='Path(s) to the documentation file(s) to test') |
| 478 | + parser.add_argument('--verbose', '-v', action='store_true', |
| 479 | + help='Enable verbose output with detailed diff information') |
| 480 | + parser.add_argument('--endpoint', '-e', default=None, |
| 481 | + help='Custom OpenSearch endpoint (default: http://localhost:9200)') |
| 482 | + parser.add_argument('--list', '-l', action='store_true', |
| 483 | + help='List all available documentation files') |
| 484 | + |
| 485 | + args = parser.parse_args() |
| 486 | + |
| 487 | + if args.list: |
| 488 | + list_available_docs() |
| 489 | + return |
| 490 | + |
| 491 | + # If no file paths provided, run the default unittest behavior |
| 492 | + if not args.file_paths: |
| 493 | + print("No specific files provided. Running full doctest suite...") |
| 494 | + # Run the standard unittest discovery |
| 495 | + unittest.main(module=None, argv=['test_docs.py'], exit=False) |
| 496 | + return |
| 497 | + |
| 498 | + # Single file testing mode |
| 499 | + all_success = True |
| 500 | + total_files = len(args.file_paths) |
| 501 | + |
| 502 | + for i, file_path in enumerate(args.file_paths, 1): |
| 503 | + if total_files > 1: |
| 504 | + print(f"\n{'='*60}") |
| 505 | + print(f"Testing file {i}/{total_files}: {file_path}") |
| 506 | + print('='*60) |
| 507 | + |
| 508 | + # Find the actual file path (handles both full paths and just filenames) |
| 509 | + actual_file_path = find_doc_file(file_path) |
| 510 | + if not actual_file_path: |
| 511 | + print(f"Skipping {file_path} - file not found") |
| 512 | + all_success = False |
| 513 | + continue |
| 514 | + |
| 515 | + success = run_single_doctest( |
| 516 | + actual_file_path, |
| 517 | + verbose=args.verbose, |
| 518 | + endpoint=args.endpoint |
| 519 | + ) |
| 520 | + |
| 521 | + if not success: |
| 522 | + all_success = False |
| 523 | + |
| 524 | + if total_files > 1: |
| 525 | + print(f"\n{'='*60}") |
| 526 | + print(f"SUMMARY: Tested {total_files} files") |
| 527 | + if all_success: |
| 528 | + print("All tests passed!") |
| 529 | + else: |
| 530 | + print("Some tests failed!") |
| 531 | + print('='*60) |
| 532 | + |
| 533 | + sys.exit(0 if all_success else 1) |
| 534 | + |
| 535 | + |
| 536 | +if __name__ == '__main__': |
| 537 | + main() |
0 commit comments