diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 516015ed..01160ee3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,6 +38,10 @@ jobs: run: | pip install ford + # Install ffmpeg for animation generation + sudo apt-get update + sudo apt-get install -y ffmpeg + # Install fpm (Fortran Package Manager) - same version as CI wget https://github.com/fortran-lang/fpm/releases/download/v0.12.0/fpm-0.12.0-linux-x86_64-gcc-12 chmod +x fpm-0.12.0-linux-x86_64-gcc-12 @@ -55,6 +59,10 @@ jobs: # Run all examples to generate outputs make example + # Build and run special examples (including animation) + chmod +x scripts/compile_special_examples.sh + ./scripts/compile_special_examples.sh || true + # Copy generated outputs to media directory (preserving structure) for dir in output/example/fortran/*/; do example_name=$(basename "$dir") diff --git a/Makefile b/Makefile index e50da6be..ed9e5098 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ build: # Build and run the examples example: create_build_dirs fpm run --example $(FPM_FLAGS_TEST) $(ARGS) + # Build and run special examples that need manual compilation + @if [ -z "$(ARGS)" ]; then \ + ./scripts/compile_special_examples.sh 2>/dev/null || true; \ + fi # Build and run the apps for debugging debug: @@ -89,7 +93,19 @@ doc: ford README.md # Copy example media files to doc build directory for proper linking mkdir -p build/doc/media/examples + # Copy from doc/media if it exists (GitHub Actions workflow populates this) if [ -d doc/media/examples ]; then cp -r doc/media/examples/* build/doc/media/examples/ 2>/dev/null || true; fi + # Also copy directly from output directory if available (for local builds) + for dir in output/example/fortran/*/; do \ + if [ -d "$$dir" ]; then \ + example_name=$$(basename "$$dir"); \ + mkdir -p "build/doc/media/examples/$$example_name"; \ + cp "$$dir"*.png "build/doc/media/examples/$$example_name/" 2>/dev/null || true; \ + cp "$$dir"*.txt "build/doc/media/examples/$$example_name/" 2>/dev/null || true; \ + cp "$$dir"*.pdf "build/doc/media/examples/$$example_name/" 2>/dev/null || true; \ + cp "$$dir"*.mp4 "build/doc/media/examples/$$example_name/" 2>/dev/null || true; \ + fi; \ + done # Generate coverage report coverage: diff --git a/doc/example/animation.md b/doc/example/animation.md index 14bde588..b06f5006 100644 --- a/doc/example/animation.md +++ b/doc/example/animation.md @@ -3,7 +3,7 @@ title: Animation # Animation -Create animated plots with enhanced FFmpeg pipe reliability and comprehensive error recovery. +Create animated plots with enhanced FFmpeg pipe reliability and GitHub Pages integration. ## Quick Start @@ -18,14 +18,38 @@ integer :: status ! Create animation anim = FuncAnimation(update_func, frames=60, interval=50, fig=fig) -! Save with enhanced error handling -call anim%save("animation.mp4", fps=24, status=status) +! Save to GitHub Pages structure +call create_output_directory() +call save_animation(anim, "output/example/fortran/animation/animation.mp4", 24, status) if (status /= 0) print *, "Enhanced error recovery available" ``` -## Enhanced FFmpeg Integration (Issue #186) +## GitHub Pages Integration + +**Download Animation**: [animation.mp4](../media/examples/animation/animation.mp4) + +The animation example generates MP4 files in a structured directory: + +``` +output/example/fortran/animation/animation.mp4 +``` + +This path integrates seamlessly with GitHub Pages documentation, making animated examples directly accessible via web links. + +### Output Structure + +```fortran +! Create directory and save animation +call create_output_directory() +call save_animation(anim, "output/example/fortran/animation/animation.mp4", 24, status) +``` + +**Result**: Animation saved to `output/example/fortran/animation/animation.mp4` for GitHub Pages access. + +## Enhanced FFmpeg Integration **Latest Improvements:** +- GitHub Pages MP4 download integration - Enhanced pipe reliability with robust error recovery - Cross-platform file validation and error diagnostics - Improved Windows binary pipe handling @@ -74,9 +98,10 @@ program save_animation_demo ! Create animation with figure reference anim = FuncAnimation(update_wave, frames=NFRAMES, interval=50, fig=fig) - ! Save as MP4 video with 24 fps + ! Save as MP4 video with 24 fps to GitHub Pages structure print *, "Saving animation as MP4..." - call save_animation_with_error_handling(anim, "animation.mp4", 24) + call create_output_directory() + call save_animation_with_error_handling(anim, "output/example/fortran/animation/animation.mp4", 24) contains @@ -100,7 +125,7 @@ contains integer, intent(in) :: fps integer :: status - call anim%save(filename, fps, status) + call save_animation(anim, filename, fps, status) select case (status) case (0) @@ -121,6 +146,17 @@ contains print *, "✗ Unknown error (status:", status, ") - check system diagnostics" end select end subroutine save_animation_with_error_handling + + subroutine create_output_directory() + integer :: mkdir_status + + ! Create directory structure for GitHub Pages integration + call execute_command_line("mkdir -p output/example/fortran/animation", & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + print *, "Warning: Could not create output directory structure" + end if + end subroutine create_output_directory end program save_animation_demo ``` @@ -176,37 +212,13 @@ ffprobe -v error -show_format animation.mp4 file animation.mp4 # Should show: ISO Media, MP4 v2 ``` -## Example Code Structure - -```fortran -! Initialize animation -call anim%init(fps=30, duration=5.0) - -! Generate frames -do i = 1, n_frames - ! Update data - call update_data(t) - - ! Plot frame - call fig%clear() - call fig%add_plot(x, y) - - ! Add frame - call anim%add_frame(fig) -end do - -! Save video -call anim%save('animation.mp4') -``` ## Windows-Specific Examples -### Handling Paths with Spaces +### Cross-Platform Paths ```fortran -! Windows paths with spaces work automatically -character(len=256) :: output_file -output_file = "C:\Users\User Name\Documents\animation.mp4" -call anim%save(output_file, fps=24, status=status) +! GitHub Pages structure works on all platforms +call save_animation(anim, "output/example/fortran/animation/animation.mp4", 24, status) ``` ### Error Handling for Windows @@ -221,19 +233,17 @@ else if (status == -1) then end if ``` -### Directory Creation -```fortran -! Create output directory on Windows -character(len=256) :: output_dir -output_dir = "animations" -if (is_windows()) then - call system('mkdir "' // trim(output_dir) // '" 2>NUL') -else - call system('mkdir -p "' // trim(output_dir) // '"') -end if -``` -## Output +## Accessing Animation Files + +**Local Development**: +- Generated file: `output/example/fortran/animation/animation.mp4` +- Use any video player to view locally + +**GitHub Pages**: +- Download link: `[animation.mp4](../media/examples/animation/animation.mp4)` +- Direct browser access from documentation +- Integrated with example documentation workflow ## Troubleshooting (Enhanced) diff --git a/example/fortran/animation/save_animation_demo.f90 b/example/fortran/animation/save_animation_demo.f90 index 85ccd44c..99a99c12 100644 --- a/example/fortran/animation/save_animation_demo.f90 +++ b/example/fortran/animation/save_animation_demo.f90 @@ -29,9 +29,10 @@ program save_animation_demo ! Create animation with figure reference anim = FuncAnimation(update_wave, frames=NFRAMES, interval=50, fig=fig) - ! Save as MP4 video with 24 fps + ! Save as MP4 video with 24 fps to GitHub Pages structure print *, "Saving animation as MP4..." - call save_animation_with_error_handling(anim, "animation.mp4", 24) + call create_output_directory() + call save_animation_with_error_handling(anim, "output/example/fortran/animation/animation.mp4", 24) contains @@ -55,7 +56,7 @@ subroutine save_animation_with_error_handling(anim, filename, fps) integer, intent(in) :: fps integer :: status - call anim%save(filename, fps, status) + call save_animation(anim, filename, fps, status) select case (status) case (0) @@ -79,4 +80,15 @@ subroutine save_animation_with_error_handling(anim, filename, fps) end select end subroutine save_animation_with_error_handling + subroutine create_output_directory() + integer :: mkdir_status + + ! Create directory structure for GitHub Pages integration + call execute_command_line("mkdir -p output/example/fortran/animation", & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + print *, "Warning: Could not create output directory structure" + end if + end subroutine create_output_directory + end program save_animation_demo \ No newline at end of file diff --git a/scripts/compile_special_examples.sh b/scripts/compile_special_examples.sh new file mode 100755 index 00000000..82adfeb1 --- /dev/null +++ b/scripts/compile_special_examples.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Build special examples that require manual compilation + +# Exit on error +set -e + +echo "Building special examples..." + +# Find the build directory with module files +BUILD_DIR=$(find build -name "fortplot.mod" -type f 2>/dev/null | head -1 | xargs dirname) +LIB_DIR=$(find build -name "libfortplot.a" -type f 2>/dev/null | head -1 | xargs dirname) + +if [ -z "$BUILD_DIR" ] || [ -z "$LIB_DIR" ]; then + echo "Error: fortplot library not built. Run 'make build' first." + exit 1 +fi + +# Build animation example +echo "Building animation example..." +mkdir -p output/example/fortran/animation +gfortran -I "$BUILD_DIR" -o save_animation_demo_temp \ + example/fortran/animation/save_animation_demo.f90 \ + "$LIB_DIR/libfortplot.a" -lm + +# Run animation example to generate MP4 +echo "Generating animation..." +./save_animation_demo_temp +rm -f save_animation_demo_temp + +echo "Special examples built successfully!" \ No newline at end of file diff --git a/src/fortplot_animation.f90 b/src/fortplot_animation.f90 index 9d59b17d..3ec86174 100644 --- a/src/fortplot_animation.f90 +++ b/src/fortplot_animation.f90 @@ -12,6 +12,9 @@ module fortplot_animation public :: animate_interface public :: save_animation + ! Register the save implementation on module initialization + logical, save :: impl_registered = .false. + contains ! Wrapper to maintain backward compatibility for save method @@ -21,7 +24,26 @@ subroutine save_animation(anim, filename, fps, status) integer, intent(in), optional :: fps integer, intent(out), optional :: status + call register_save_implementation() call save_animation_full(anim, filename, fps, status) end subroutine save_animation + ! Type-bound procedure for animation save method implementation + subroutine animation_save_impl(anim, filename, fps, status) + class(animation_t), intent(inout) :: anim + character(len=*), intent(in) :: filename + integer, intent(in), optional :: fps + integer, intent(out), optional :: status + + call save_animation_full(anim, filename, fps, status) + end subroutine animation_save_impl + + ! Register the save implementation pointer + subroutine register_save_implementation() + if (.not. impl_registered) then + save_animation_impl => animation_save_impl + impl_registered = .true. + end if + end subroutine register_save_implementation + end module fortplot_animation \ No newline at end of file diff --git a/src/fortplot_animation_core.f90 b/src/fortplot_animation_core.f90 index ad5acc61..bc0913d3 100644 --- a/src/fortplot_animation_core.f90 +++ b/src/fortplot_animation_core.f90 @@ -31,10 +31,27 @@ end subroutine animate_interface procedure :: set_save_frames procedure :: save_frame_sequence procedure :: set_figure + procedure :: save end type animation_t + ! Animation save interface to avoid circular dependency + abstract interface + subroutine save_animation_interface(anim, filename, fps, status) + import :: animation_t + class(animation_t), intent(inout) :: anim + character(len=*), intent(in) :: filename + integer, intent(in), optional :: fps + integer, intent(out), optional :: status + end subroutine save_animation_interface + end interface + + ! External save implementation procedure pointer + procedure(save_animation_interface), pointer :: save_animation_impl => null() + public :: FuncAnimation public :: animate_interface + public :: save_animation_interface + public :: save_animation_impl contains @@ -147,6 +164,33 @@ subroutine cpu_time_delay(seconds) end do end subroutine cpu_time_delay + subroutine save(self, filename, fps, status) + !! Save animation to video file - delegates to full pipeline implementation + class(animation_t), intent(inout) :: self + character(len=*), intent(in) :: filename + integer, intent(in), optional :: fps + integer, intent(out), optional :: status + + ! Attempt to register implementation if not done + call try_register_save_implementation() + + if (.not. associated(save_animation_impl)) then + if (present(status)) status = -1 + call log_error_with_remediation("Animation save implementation not initialized", & + "Import fortplot_animation module to register the save implementation") + return + end if + + ! Call the facade save_animation wrapper to avoid circular dependency + call save_animation_impl(self, filename, fps, status) + end subroutine save + + subroutine try_register_save_implementation() + !! Attempt to register save implementation (will be set by facade module) + !! This is a no-op if the facade module hasn't been imported + continue + end subroutine try_register_save_implementation + subroutine log_error_with_remediation(error_msg, remediation_msg) character(len=*), intent(in) :: error_msg, remediation_msg diff --git a/src/fortplot_figure_base.f90 b/src/fortplot_figure_base.f90 index 7ec99769..eb1e0b7a 100644 --- a/src/fortplot_figure_base.f90 +++ b/src/fortplot_figure_base.f90 @@ -105,6 +105,7 @@ module fortplot_figure_base procedure :: initialize_default_subplot procedure :: set_ydata procedure :: add_plot + procedure :: clear end type figure_t contains @@ -403,5 +404,35 @@ subroutine add_plot(self, x, y, label, linestyle, color, marker) end if end subroutine add_plot + subroutine clear(self) + !! Clear all plots and reset figure to initial state + class(figure_t), intent(inout) :: self + integer :: i + + ! Clear all plots + self%plot_count = 0 + self%rendered = .false. + + ! Reset subplot plots + if (allocated(self%subplots)) then + do i = 1, size(self%subplots) + self%subplots(i)%plot_count = 0 + end do + end if + + ! Clear streamlines and annotations if allocated + if (allocated(self%streamlines)) then + deallocate(self%streamlines) + end if + + if (allocated(self%annotations)) then + deallocate(self%annotations) + self%annotation_count = 0 + end if + + if (allocated(self%arrow_data)) then + deallocate(self%arrow_data) + end if + end subroutine clear end module fortplot_figure_base \ No newline at end of file diff --git a/test/test_animation_example_output_structure.f90 b/test/test_animation_example_output_structure.f90 new file mode 100644 index 00000000..cd85a4c6 --- /dev/null +++ b/test/test_animation_example_output_structure.f90 @@ -0,0 +1,277 @@ +program test_animation_example_output_structure + ! GIVEN-WHEN-THEN Documentation: + ! + ! GIVEN the save_animation_demo example should output to structured directory + ! WHEN the example is run with modified output path + ! THEN the MP4 should be created at output/example/fortran/animation/animation.mp4 + ! + ! GIVEN the animation example output in GitHub Pages structure + ! WHEN the file is accessed via the expected relative path + ! THEN it should match the documentation link format + ! + ! This implements RED phase testing for Issue #178: + ! Validates that animation example creates MP4 in correct GitHub Pages location + + use fortplot + use fortplot_pipe, only: check_ffmpeg_available + use fortplot_security, only: safe_remove_file + use fortplot_system_runtime, only: is_windows + use iso_fortran_env, only: real64 + implicit none + + logical :: ffmpeg_available, on_windows + character(len=256) :: ci_env + integer :: status + + ! Skip on Windows CI - FFmpeg pipe issues + on_windows = is_windows() + call get_environment_variable("CI", ci_env, status=status) + + if (on_windows .and. status == 0) then + print *, "SKIPPED: Animation example output structure tests on Windows CI" + stop 0 + end if + + ! Check if ffmpeg is available + ffmpeg_available = check_ffmpeg_available() + if (.not. ffmpeg_available) then + print *, "XFAIL: Animation example output structure tests require FFmpeg" + print *, "Expected failure - FFmpeg not available" + stop 77 ! Standard exit code for skipped tests + end if + + ! Run RED phase tests + call test_example_output_directory_compliance() + call test_example_filename_consistency() + call test_documentation_path_mapping() + + print *, "All animation example output structure tests passed!" + +contains + + subroutine test_example_output_directory_compliance() + ! GIVEN-WHEN-THEN: + ! GIVEN save_animation_demo should create output in GitHub Pages structure + ! WHEN animation is saved with structured path + ! THEN file should appear at output/example/fortran/animation/animation.mp4 + + character(len=512) :: expected_output_path, expected_dir + character(len=512) :: working_dir_file, documentation_path + logical :: structured_file_exists, working_dir_file_exists + logical :: dir_exists + integer :: mkdir_status, i + + ! Test data setup + integer, parameter :: NFRAMES = 5, NPOINTS = 20 + type(figure_t) :: test_fig + type(animation_t) :: anim + real(real64), dimension(NPOINTS) :: x_data, y_data + + ! Expected GitHub Pages structure paths + expected_dir = "output/example/fortran/animation" + expected_output_path = trim(expected_dir) // "/animation.mp4" + working_dir_file = "animation.mp4" ! Current example output location + documentation_path = "media/examples/animation/animation.mp4" + + ! Create expected directory structure + call execute_command_line("mkdir -p " // expected_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create expected output directory structure" + end if + + ! Verify directory creation + inquire(file=expected_dir, exist=dir_exists) + if (.not. dir_exists) then + error stop "Expected GitHub Pages directory structure not created" + end if + + ! Create test animation matching save_animation_demo example + do i = 1, NPOINTS + x_data(i) = real(i-1, real64) * 2.0_real64 * 3.14159_real64 & + / real(NPOINTS-1, real64) + end do + y_data = sin(x_data) + + call test_fig%initialize(width=800, height=600) + call test_fig%add_plot(x_data, y_data, label='animated wave') + call test_fig%set_title('Animation Save Demo') + call test_fig%set_xlabel('x') + call test_fig%set_ylabel('y') + call test_fig%set_xlim(0.0_real64, 2.0_real64 * 3.14159_real64) + call test_fig%set_ylim(-1.5_real64, 1.5_real64) + + ! Create animation matching example + anim = FuncAnimation(update_example_wave, frames=NFRAMES, & + interval=50, fig=test_fig) + + ! Test 1: Save to structured directory (desired behavior) + call anim%save(expected_output_path, 24) + + inquire(file=expected_output_path, exist=structured_file_exists) + if (.not. structured_file_exists) then + error stop "Animation not saved to expected GitHub Pages structure" + end if + + ! Test 2: Verify working directory file doesn't exist (current issue) + inquire(file=working_dir_file, exist=working_dir_file_exists) + if (working_dir_file_exists) then + ! Clean up unexpected file + block + logical :: remove_success + call safe_remove_file(working_dir_file, remove_success) + end block + end if + + ! Cleanup structured file + block + logical :: remove_success + call safe_remove_file(expected_output_path, remove_success) + if (.not. remove_success) then + print *, "Warning: Could not clean up structured output file" + end if + end block + + contains + subroutine update_example_wave(frame) + integer, intent(in) :: frame + real(real64) :: phase + + ! Match the phase calculation from save_animation_demo + phase = real(frame - 1, real64) * 2.0_real64 * 3.14159_real64 & + / real(NFRAMES, real64) + + ! Update y data with animated wave + y_data = sin(x_data + phase) * cos(phase * 0.5_real64) + + ! Update plot data + call test_fig%set_ydata(1, y_data) + end subroutine update_example_wave + + end subroutine test_example_output_directory_compliance + + subroutine test_example_filename_consistency() + ! GIVEN-WHEN-THEN: + ! GIVEN animation example should use consistent filename + ! WHEN example outputs "animation.mp4" + ! THEN filename should match documentation references + + character(len=512) :: expected_filename, alternative_filename + character(len=512) :: test_path, base_dir + logical :: filename_consistent + integer :: mkdir_status + + expected_filename = "animation.mp4" + alternative_filename = "save_animation_demo.mp4" ! Should NOT be used + base_dir = "output/example/fortran/animation" + test_path = trim(base_dir) // "/" // expected_filename + + ! Create directory + call execute_command_line("mkdir -p " // base_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create directory for filename test" + end if + + ! Verify expected filename format is valid + filename_consistent = .true. + + ! Check that filename matches documentation expectations + if (len_trim(expected_filename) == 0) then + filename_consistent = .false. + end if + + if (index(expected_filename, ".mp4") == 0) then + filename_consistent = .false. + end if + + ! Verify filename is not program-specific (should be generic) + if (index(expected_filename, "demo") /= 0 .or. & + index(expected_filename, "save") /= 0) then + filename_consistent = .false. + end if + + if (.not. filename_consistent) then + error stop "Animation filename format inconsistent with documentation" + end if + + ! Test path construction + if (index(test_path, expected_filename) == 0) then + error stop "Failed to construct consistent file path" + end if + + end subroutine test_example_filename_consistency + + subroutine test_documentation_path_mapping() + ! GIVEN-WHEN-THEN: + ! GIVEN output files in GitHub Pages directory structure + ! WHEN documentation references animation files + ! THEN paths should map correctly from output to documentation + + character(len=512) :: output_path, doc_path, relative_path + character(len=512) :: expected_output, expected_doc, expected_relative + logical :: mapping_valid + + ! Expected path mappings for GitHub Pages deployment + expected_output = "output/example/fortran/animation/animation.mp4" + expected_doc = "build/doc/media/examples/animation/animation.mp4" + expected_relative = "../media/examples/animation/animation.mp4" + + output_path = expected_output + doc_path = expected_doc + relative_path = expected_relative + + mapping_valid = .true. + + ! Verify output path format + if (index(output_path, "output/example") == 0) then + mapping_valid = .false. + end if + + if (index(output_path, "animation/animation.mp4") == 0) then + mapping_valid = .false. + end if + + ! Verify documentation path format + if (index(doc_path, "build/doc/media") == 0) then + mapping_valid = .false. + end if + + if (index(doc_path, "examples/animation") == 0) then + mapping_valid = .false. + end if + + ! Verify relative path format for markdown links + if (index(relative_path, "../media/examples") == 0) then + mapping_valid = .false. + end if + + if (index(relative_path, "animation.mp4") == 0) then + mapping_valid = .false. + end if + + if (.not. mapping_valid) then + error stop "Documentation path mapping invalid for GitHub Pages" + end if + + ! Test that paths have consistent depth structure + if (count_path_separators(output_path) /= count_path_separators(doc_path)) then + ! This is expected - they have different base directories + ! but the relative structure after base should be similar + end if + + contains + integer function count_path_separators(path) + character(len=*), intent(in) :: path + integer :: i, count + + count = 0 + do i = 1, len_trim(path) + if (path(i:i) == '/') count = count + 1 + end do + count_path_separators = count + end function count_path_separators + + end subroutine test_documentation_path_mapping + +end program test_animation_example_output_structure \ No newline at end of file diff --git a/test/test_animation_github_pages_integration.f90 b/test/test_animation_github_pages_integration.f90 new file mode 100644 index 00000000..69f493f8 --- /dev/null +++ b/test/test_animation_github_pages_integration.f90 @@ -0,0 +1,401 @@ +program test_animation_github_pages_integration + ! GIVEN-WHEN-THEN Documentation: + ! + ! GIVEN an animation example that should output MP4 to GitHub Pages structure + ! WHEN the animation is saved with structured output directory + ! THEN the MP4 file should be created in the correct GitHub Pages location + ! + ! GIVEN a GitHub Pages deployment structure with media directories + ! WHEN documentation is generated linking to animation files + ! THEN the animation MP4 should be accessible via download links + ! + ! GIVEN an animation output directory structure for GitHub Pages + ! WHEN multiple animations are generated in the same session + ! THEN each animation should be saved to its appropriate subdirectory + ! + ! This implements RED phase testing for Issue #178: + ! Missing MP4 download link in GitHub Pages animation example + + use fortplot + use fortplot_animation + use fortplot_pipe, only: check_ffmpeg_available + use fortplot_security, only: safe_remove_file + use fortplot_system_runtime, only: is_windows + use iso_fortran_env, only: real64 + implicit none + + logical :: ffmpeg_available, on_windows + character(len=256) :: ci_env + integer :: status + + ! Module variables for nested procedures + type(figure_t) :: test_fig + real(real64), dimension(10) :: test_x, test_y + + ! Skip on Windows CI - FFmpeg pipe issues + on_windows = is_windows() + call get_environment_variable("CI", ci_env, status=status) + + if (on_windows .and. status == 0) then + print *, "SKIPPED: GitHub Pages animation tests on Windows CI" + stop 0 + end if + + ! Check if ffmpeg is available + ffmpeg_available = check_ffmpeg_available() + if (.not. ffmpeg_available) then + print *, "XFAIL: GitHub Pages animation tests require FFmpeg" + print *, "Expected failure - FFmpeg not available" + stop 77 ! Standard exit code for skipped tests + end if + + ! Run RED phase tests + call test_animation_output_directory_structure() + call test_github_pages_media_directory_creation() + call test_animation_file_accessibility() + call test_documentation_link_validation() + call test_multiple_animation_organization() + + print *, "All GitHub Pages animation integration tests passed!" + +contains + + subroutine test_animation_output_directory_structure() + ! GIVEN-WHEN-THEN: + ! GIVEN an animation demo that needs GitHub Pages integration + ! WHEN the animation is saved to structured output directory + ! THEN the file should be created at output/example/fortran/animation/ + + type(animation_t) :: anim + character(len=512) :: output_path, expected_dir + logical :: file_exists, dir_exists + integer :: i, mkdir_status + + ! Expected GitHub Pages structure + expected_dir = "output/example/fortran/animation" + output_path = trim(expected_dir) // "/animation.mp4" + + ! Create directory structure if it doesn't exist + call execute_command_line("mkdir -p " // expected_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create output directory structure" + end if + + ! Verify directory was created + inquire(file=expected_dir, exist=dir_exists) + if (.not. dir_exists) then + error stop "Expected directory structure was not created" + end if + + ! Create test animation + test_x = [(real(i, real64), i=1,10)] + test_y = sin(test_x) + + call test_fig%initialize(width=800, height=600) + call test_fig%add_plot(test_x, test_y, label='GitHub Pages test wave') + call test_fig%set_title('GitHub Pages Animation Demo') + + anim = FuncAnimation(update_github_pages_animation, & + frames=10, interval=50, fig=test_fig) + + ! Save animation to structured directory + call anim%save(output_path) + + ! Verify file was created in correct location + inquire(file=output_path, exist=file_exists) + if (.not. file_exists) then + error stop "Animation MP4 not created in GitHub Pages structure" + end if + + ! Clean up + block + logical :: remove_success + call safe_remove_file(output_path, remove_success) + if (.not. remove_success) then + print *, "Warning: Could not clean up test file: " // trim(output_path) + end if + end block + + end subroutine test_animation_output_directory_structure + + subroutine update_github_pages_animation(frame) + integer, intent(in) :: frame + real(real64) :: phase + + phase = real(frame, real64) * 0.5_real64 + test_y = sin(test_x + phase) + call test_fig%set_ydata(1, test_y) + end subroutine update_github_pages_animation + + subroutine test_github_pages_media_directory_creation() + ! GIVEN-WHEN-THEN: + ! GIVEN a documentation build process for GitHub Pages + ! WHEN animation files are processed for web deployment + ! THEN media directory structure should be created automatically + + character(len=512) :: media_dir, doc_media_dir + logical :: media_exists, doc_media_exists + integer :: mkdir_status + + media_dir = "output/example/fortran/animation" + doc_media_dir = "build/doc/media/examples/animation" + + ! Test output directory creation + call execute_command_line("mkdir -p " // media_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create media output directory" + end if + + ! Test documentation media directory creation + call execute_command_line("mkdir -p " // doc_media_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create documentation media directory" + end if + + ! Verify both directories exist + inquire(file=media_dir, exist=media_exists) + inquire(file=doc_media_dir, exist=doc_media_exists) + + if (.not. media_exists) then + error stop "Output media directory was not created" + end if + + if (.not. doc_media_exists) then + error stop "Documentation media directory was not created" + end if + + ! Clean up test directories + call execute_command_line("rmdir " // doc_media_dir // " 2>/dev/null || true") + call execute_command_line("rmdir " // media_dir // " 2>/dev/null || true") + + end subroutine test_github_pages_media_directory_creation + + subroutine test_animation_file_accessibility() + ! GIVEN-WHEN-THEN: + ! GIVEN an animation MP4 file in GitHub Pages structure + ! WHEN the file is accessed via relative path + ! THEN the file should be readable and have valid MP4 header + + type(animation_t) :: anim + character(len=512) :: test_file, expected_dir + logical :: file_exists, is_readable + integer :: i, unit_num, io_stat, mkdir_status + character(len=4) :: mp4_header + + expected_dir = "output/example/fortran/animation" + test_file = trim(expected_dir) // "/test_accessibility.mp4" + + ! Create directory structure + call execute_command_line("mkdir -p " // expected_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create directory for accessibility test" + end if + + ! Create test animation + test_x(1:5) = [(real(i, real64), i=1,5)] + test_y(1:5) = test_x(1:5)**2 + + call test_fig%initialize(width=400, height=300) + call test_fig%add_plot(test_x(1:5), test_y(1:5)) + + anim = FuncAnimation(update_accessibility_test, & + frames=3, interval=100, fig=test_fig) + + ! Save animation + call anim%save(test_file) + + ! Verify file exists + inquire(file=test_file, exist=file_exists) + if (.not. file_exists) then + error stop "Test animation file was not created" + end if + + ! Test file accessibility - try to read MP4 header + open(newunit=unit_num, file=test_file, status='old', & + access='stream', form='unformatted', iostat=io_stat) + if (io_stat /= 0) then + error stop "Animation file is not accessible for reading" + end if + + ! Read first 4 bytes to check for valid MP4-like format + read(unit_num, iostat=io_stat) mp4_header + close(unit_num) + + if (io_stat /= 0) then + error stop "Could not read animation file header" + end if + + ! MP4 files should have some recognizable binary header + ! (not checking exact format, just ensuring it's binary data) + if (len_trim(mp4_header) == 0) then + error stop "Animation file appears to be empty or invalid" + end if + + ! Clean up + block + logical :: remove_success + call safe_remove_file(test_file, remove_success) + if (.not. remove_success) then + print *, "Warning: Could not clean up accessibility test file" + end if + end block + + end subroutine test_animation_file_accessibility + + subroutine update_accessibility_test(frame) + integer, intent(in) :: frame + test_y(1:5) = test_x(1:5)**2 + real(frame, real64) * 0.1_real64 + call test_fig%set_ydata(1, test_y(1:5)) + end subroutine update_accessibility_test + + subroutine test_documentation_link_validation() + ! GIVEN-WHEN-THEN: + ! GIVEN animation MP4 files in GitHub Pages directory structure + ! WHEN documentation links are generated for download + ! THEN the relative paths should be correctly formatted + + character(len=512) :: relative_path, expected_path + character(len=1024) :: markdown_link + logical :: path_format_valid + + ! Test relative path format for GitHub Pages + relative_path = "../media/examples/animation/animation.mp4" + expected_path = "media/examples/animation/animation.mp4" + + ! Verify path formatting follows GitHub Pages conventions + path_format_valid = .true. + + ! Check for proper file extension + if (index(relative_path, ".mp4") == 0) then + path_format_valid = .false. + end if + + ! Check for proper directory structure + if (index(relative_path, "animation") == 0) then + path_format_valid = .false. + end if + + if (.not. path_format_valid) then + error stop "Animation path format invalid for GitHub Pages" + end if + + ! Test markdown link generation format + markdown_link = "📹 **Animation Video**: [animation.mp4](" // & + trim(relative_path) // ")" + + if (len_trim(markdown_link) == 0) then + error stop "Failed to generate valid markdown download link" + end if + + ! Verify link contains required components + if (index(markdown_link, "animation.mp4") == 0) then + error stop "Download link missing filename" + end if + + if (index(markdown_link, "](") == 0) then + error stop "Download link missing markdown syntax" + end if + + end subroutine test_documentation_link_validation + + subroutine test_multiple_animation_organization() + ! GIVEN-WHEN-THEN: + ! GIVEN multiple animation examples in the same project + ! WHEN each animation is saved to GitHub Pages structure + ! THEN each should have its own organized subdirectory + + type(animation_t) :: anim1, anim2 + character(len=512) :: base_dir, file1, file2, subdir1, subdir2 + logical :: file1_exists, file2_exists + integer :: i, mkdir_status + + base_dir = "output/example/fortran" + subdir1 = trim(base_dir) // "/animation" + subdir2 = trim(base_dir) // "/other_animation" + + file1 = trim(subdir1) // "/demo1.mp4" + file2 = trim(subdir2) // "/demo2.mp4" + + ! Create both directory structures + call execute_command_line("mkdir -p " // subdir1, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create first animation subdirectory" + end if + + call execute_command_line("mkdir -p " // subdir2, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create second animation subdirectory" + end if + + ! Create first test animation + test_x(1:8) = [(real(i, real64), i=1,8)] + test_y(1:8) = cos(test_x(1:8)) + + call test_fig%initialize(width=600, height=400) + call test_fig%add_plot(test_x(1:8), test_y(1:8), label='Demo 1') + + anim1 = FuncAnimation(update_demo1, frames=5, interval=80, fig=test_fig) + call anim1%save(file1) + + ! Create second test animation + test_y(1:8) = sin(test_x(1:8) * 2.0_real64) + call test_fig%clear() + call test_fig%add_plot(test_x(1:8), test_y(1:8), label='Demo 2') + + anim2 = FuncAnimation(update_demo2, frames=5, interval=80, fig=test_fig) + call anim2%save(file2) + + ! Verify both files were created in correct subdirectories + inquire(file=file1, exist=file1_exists) + inquire(file=file2, exist=file2_exists) + + if (.not. file1_exists) then + error stop "First animation not created in organized subdirectory" + end if + + if (.not. file2_exists) then + error stop "Second animation not created in organized subdirectory" + end if + + ! Clean up both test files + block + logical :: remove1_success, remove2_success + call safe_remove_file(file1, remove1_success) + call safe_remove_file(file2, remove2_success) + + if (.not. remove1_success) then + print *, "Warning: Could not clean up first test animation" + end if + if (.not. remove2_success) then + print *, "Warning: Could not clean up second test animation" + end if + end block + + ! Clean up test directories + call execute_command_line("rmdir " // subdir2 // " 2>/dev/null || true") + + end subroutine test_multiple_animation_organization + + subroutine update_demo1(frame) + integer, intent(in) :: frame + real(real64) :: shift + shift = real(frame, real64) * 0.3_real64 + test_y(1:8) = cos(test_x(1:8) + shift) + call test_fig%set_ydata(1, test_y(1:8)) + end subroutine update_demo1 + + subroutine update_demo2(frame) + integer, intent(in) :: frame + real(real64) :: amplitude + amplitude = 1.0_real64 + real(frame, real64) * 0.2_real64 + test_y(1:8) = sin(test_x(1:8) * 2.0_real64) * amplitude + call test_fig%set_ydata(1, test_y(1:8)) + end subroutine update_demo2 + +end program test_animation_github_pages_integration \ No newline at end of file diff --git a/test/test_github_pages_directory_structure.f90 b/test/test_github_pages_directory_structure.f90 new file mode 100644 index 00000000..59a7e293 --- /dev/null +++ b/test/test_github_pages_directory_structure.f90 @@ -0,0 +1,258 @@ +program test_github_pages_directory_structure + ! GIVEN-WHEN-THEN Documentation: + ! + ! GIVEN animation examples that should integrate with GitHub Pages deployment + ! WHEN the output directory structure is created for GitHub Pages + ! THEN the directory structure should follow expected patterns + ! + ! GIVEN GitHub Pages media directory requirements + ! WHEN documentation links reference animation files + ! THEN the relative path structure should be consistent + ! + ! This implements RED phase testing for Issue #178: + ! Tests directory structure and path mapping for GitHub Pages integration + + implicit none + + ! Run RED phase tests for directory structure requirements + call test_output_directory_structure_creation() + call test_github_pages_path_consistency() + call test_documentation_directory_mapping() + call test_relative_path_validation() + + print *, "All GitHub Pages directory structure tests passed!" + +contains + + subroutine test_output_directory_structure_creation() + ! GIVEN-WHEN-THEN: + ! GIVEN animation examples need GitHub Pages deployment structure + ! WHEN output directories are created for structured deployment + ! THEN the directory structure should match expected GitHub Pages layout + + character(len=512) :: expected_output_dir, expected_doc_dir + character(len=512) :: animation_dir, media_dir + logical :: output_dir_exists, doc_dir_exists + logical :: animation_dir_exists, media_dir_exists + integer :: mkdir_status + + ! Expected GitHub Pages directory structure + expected_output_dir = "output/example/fortran" + expected_doc_dir = "build/doc/media/examples" + animation_dir = trim(expected_output_dir) // "/animation" + media_dir = trim(expected_doc_dir) // "/animation" + + ! Test directory creation for output structure + call execute_command_line("mkdir -p " // animation_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create output animation directory structure" + end if + + ! Test directory creation for documentation structure + call execute_command_line("mkdir -p " // media_dir, & + exitstat=mkdir_status) + if (mkdir_status /= 0) then + error stop "Failed to create documentation media directory structure" + end if + + ! Verify directories were created + inquire(file=expected_output_dir, exist=output_dir_exists) + inquire(file=expected_doc_dir, exist=doc_dir_exists) + inquire(file=animation_dir, exist=animation_dir_exists) + inquire(file=media_dir, exist=media_dir_exists) + + if (.not. output_dir_exists) then + error stop "Output directory structure not created correctly" + end if + + if (.not. doc_dir_exists) then + error stop "Documentation directory structure not created correctly" + end if + + if (.not. animation_dir_exists) then + error stop "Animation subdirectory not created in output structure" + end if + + if (.not. media_dir_exists) then + error stop "Animation subdirectory not created in documentation structure" + end if + + ! Clean up test directories (from deepest to shallowest) + call execute_command_line("rmdir " // media_dir // " 2>/dev/null || true") + call execute_command_line("rmdir " // animation_dir // " 2>/dev/null || true") + call execute_command_line("rmdir " // trim(expected_doc_dir) // " 2>/dev/null || true") + + end subroutine test_output_directory_structure_creation + + subroutine test_github_pages_path_consistency() + ! GIVEN-WHEN-THEN: + ! GIVEN GitHub Pages deployment requires consistent path structure + ! WHEN animation files are referenced in documentation + ! THEN paths should follow consistent naming patterns + + character(len=512) :: output_path, doc_path, relative_path + character(len=512) :: expected_filename + logical :: path_consistency_valid + + expected_filename = "animation.mp4" + + ! Test path patterns for GitHub Pages consistency + output_path = "output/example/fortran/animation/" // expected_filename + doc_path = "build/doc/media/examples/animation/" // expected_filename + relative_path = "../media/examples/animation/" // expected_filename + + path_consistency_valid = .true. + + ! Validate output path structure + if (index(output_path, "output/example/fortran") == 0) then + path_consistency_valid = .false. + end if + + if (index(output_path, "animation/" // expected_filename) == 0) then + path_consistency_valid = .false. + end if + + ! Validate documentation path structure + if (index(doc_path, "build/doc/media/examples") == 0) then + path_consistency_valid = .false. + end if + + if (index(doc_path, "animation/" // expected_filename) == 0) then + path_consistency_valid = .false. + end if + + ! Validate relative path for markdown links + if (index(relative_path, "../media/examples") == 0) then + path_consistency_valid = .false. + end if + + if (index(relative_path, expected_filename) == 0) then + path_consistency_valid = .false. + end if + + if (.not. path_consistency_valid) then + error stop "GitHub Pages path consistency validation failed" + end if + + end subroutine test_github_pages_path_consistency + + subroutine test_documentation_directory_mapping() + ! GIVEN-WHEN-THEN: + ! GIVEN output files need to be accessible via GitHub Pages + ! WHEN documentation references animation files + ! THEN directory mapping should provide correct accessibility + + character(len=512) :: source_pattern, target_pattern + character(len=512) :: test_source, test_target + logical :: mapping_valid + integer :: path_depth_source, path_depth_target + + ! Test directory mapping patterns + source_pattern = "output/example/fortran/animation" + target_pattern = "build/doc/media/examples/animation" + + test_source = trim(source_pattern) // "/test.mp4" + test_target = trim(target_pattern) // "/test.mp4" + + mapping_valid = .true. + + ! Verify source directory pattern + if (len_trim(source_pattern) == 0) then + mapping_valid = .false. + end if + + if (index(source_pattern, "animation") == 0) then + mapping_valid = .false. + end if + + ! Verify target directory pattern + if (len_trim(target_pattern) == 0) then + mapping_valid = .false. + end if + + if (index(target_pattern, "animation") == 0) then + mapping_valid = .false. + end if + + ! Check directory depth consistency for mapping + path_depth_source = count_path_separators(source_pattern) + path_depth_target = count_path_separators(target_pattern) + + ! Both should have consistent depth for proper mapping + if (path_depth_source < 3 .or. path_depth_target < 4) then + mapping_valid = .false. + end if + + if (.not. mapping_valid) then + error stop "Documentation directory mapping validation failed" + end if + + end subroutine test_documentation_directory_mapping + + subroutine test_relative_path_validation() + ! GIVEN-WHEN-THEN: + ! GIVEN documentation markdown files reference animation files + ! WHEN relative paths are constructed for GitHub Pages + ! THEN the paths should be valid and properly formatted + + character(len=1024) :: markdown_link, href_path + character(len=512) :: filename, relative_path + logical :: link_format_valid + + filename = "animation.mp4" + relative_path = "../media/examples/animation/" // trim(filename) + + ! Test markdown link generation + markdown_link = "📹 **Animation Video**: [" // trim(filename) // "](" // & + trim(relative_path) // ")" + + href_path = relative_path + + link_format_valid = .true. + + ! Validate markdown link format + if (len_trim(markdown_link) == 0) then + link_format_valid = .false. + end if + + if (index(markdown_link, "[" // trim(filename) // "]") == 0) then + link_format_valid = .false. + end if + + if (index(markdown_link, "](" // trim(relative_path) // ")") == 0) then + link_format_valid = .false. + end if + + ! Validate relative path format + if (index(href_path, "../") /= 1) then + link_format_valid = .false. + end if + + if (index(href_path, ".mp4") == 0) then + link_format_valid = .false. + end if + + ! Check for proper path segments + if (index(href_path, "media/examples") == 0) then + link_format_valid = .false. + end if + + if (.not. link_format_valid) then + error stop "Relative path validation failed for GitHub Pages links" + end if + + end subroutine test_relative_path_validation + + integer function count_path_separators(path) + character(len=*), intent(in) :: path + integer :: i, count + + count = 0 + do i = 1, len_trim(path) + if (path(i:i) == '/') count = count + 1 + end do + count_path_separators = count + end function count_path_separators + +end program test_github_pages_directory_structure \ No newline at end of file