@@ -409,6 +409,84 @@ def test_anchors_ignored_for_url(app: SphinxTestApp) -> None:
409409 'info' : f'404 Client Error: Not Found for url: http://{ address } /invalid' ,
410410 }
411411
412+ @pytest .mark .sphinx (
413+ 'linkcheck' ,
414+ testroot = 'linkcheck' ,
415+ freshenv = True ,
416+ )
417+ def test_cache (app : SphinxTestApp ) -> None :
418+ app .config .linkcheck_cache = True
419+ class InternalServerErrorHandler (BaseHTTPRequestHandler ):
420+ protocol_version = 'HTTP/1.1'
421+
422+ def do_GET (self ) -> None :
423+ self .send_error (500 , 'Internal Server Error' )
424+
425+ # First run doing caching
426+ with serve_application (app , OKHandler ) as address :
427+ app .build ()
428+
429+ assert (app .outdir / 'output.json' ).exists ()
430+ output_content = (app .outdir / 'output.json' ).read_text (encoding = 'utf8' )
431+
432+ rows = [json .loads (x ) for x in output_content .splitlines ()]
433+ assert len (rows ) == 10
434+ rowsby = {row ['uri' ]: row for row in rows }
435+ assert rowsby [f'http://{ address } /' ]['status' ] == 'working'
436+ assert rowsby [f'http://{ address } /#!bar' ]['status' ] == 'working'
437+ assert rowsby [f'http://{ address } /image.png' ]['status' ] == 'working'
438+ assert rowsby [f'http://{ address } /image2.png' ]['status' ] == 'working'
439+ assert rowsby ['conf.py' ]['status' ] == 'working'
440+ assert rowsby ['path/to/notfound' ]['status' ] == 'broken'
441+ assert rowsby [f'http://{ address } /#top' ]['status' ] == 'broken'
442+
443+ assert (app .outdir / app .config .linkcheck_cache_file ).exists ()
444+ with (app .outdir / app .config .linkcheck_cache_file ).open ('r' ) as f :
445+ cache_initial = json .load (f )
446+ assert len (cache_initial ) == 5
447+ assert f'http://{ address } /' in cache_initial
448+ assert f'http://{ address } /#!bar' in cache_initial
449+ assert f'http://{ address } /image.png' in cache_initial
450+ assert 'conf.py' not in cache_initial # because it does not use http
451+ assert f'http://{ address } /#top' not in cache_initial # because it was broken
452+
453+ # Second run with cached values
454+ # Manually expire a cache item
455+ cache_initial [f'http://{ address } /image2.png' ] = 0.0
456+ with (app .outdir / app .config .linkcheck_cache_file ).open ('w' ) as f :
457+ json .dump (cache_initial , f )
458+
459+ with serve_application (app , InternalServerErrorHandler , port = int (address .split (':' )[1 ])):
460+ app .build ()
461+
462+ assert (app .outdir / 'output.json' ).exists ()
463+ output_content = (app .outdir / 'output.json' ).read_text (encoding = 'utf8' )
464+
465+ rows = [json .loads (x ) for x in output_content .splitlines ()]
466+ assert len (rows ) == 10
467+ rowsby = {row ['uri' ]: row for row in rows }
468+ assert rowsby [f'http://{ address } /' ]['status' ] == 'cached'
469+ assert rowsby [f'http://{ address } /#!bar' ]['status' ] == 'cached'
470+ assert rowsby [f'http://{ address } /image.png' ]['status' ] == 'cached'
471+ assert rowsby [f'http://{ address } /image2.png' ]['status' ] == 'broken' # because cache expired
472+ assert rowsby ['conf.py' ]['status' ] == 'working'
473+ assert rowsby ['path/to/notfound' ]['status' ] == 'broken'
474+ assert rowsby [f'http://{ address } /#top' ]['status' ] == 'broken'
475+
476+ assert (app .outdir / app .config .linkcheck_cache_file ).exists ()
477+ with (app .outdir / app .config .linkcheck_cache_file ).open ('r' ) as f :
478+ cache_after = json .load (f )
479+ assert len (cache_after ) == 5
480+ assert f'http://{ address } /' in cache_after
481+ assert f'http://{ address } /#!bar' in cache_after
482+ assert f'http://{ address } /image.png' in cache_after
483+ assert f'http://{ address } /image2.png' in cache_after
484+ assert cache_after [f'http://{ address } /image2.png' ] == 0.0
485+ assert 'conf.py' not in cache_after
486+ assert f'http://{ address } /#top' not in cache_after
487+
488+ assert all (cache_initial [uri ] == cache_after [uri ] for uri in cache_initial )
489+
412490
413491@pytest .mark .sphinx (
414492 'linkcheck' ,
0 commit comments