mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
fix: chromium memory leaks (#705)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/coverage.html
|
||||
/coverage.txt
|
||||
/coverage.txt
|
||||
/TODO.txt
|
||||
@@ -1,18 +1,11 @@
|
||||
linters-settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard # Standard section: captures all standard packages.
|
||||
- default # Default section: contains all imports that could not be matched to another section type.
|
||||
- prefix(github.com/gotenberg/gotenberg/v7) # Ensure that this is always at the top and always has a line break.
|
||||
# Skip generated files.
|
||||
# Default: true
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/gotenberg/gotenberg/v7)
|
||||
skip-generated: true
|
||||
# Skip vendor files.
|
||||
# Default: true
|
||||
skip-vendor: true
|
||||
# Enable custom order of sections.
|
||||
# If `true`, make the section order the same as the order of `sections`.
|
||||
# Default: false
|
||||
custom-order: true
|
||||
|
||||
linters:
|
||||
|
||||
11
Makefile
11
Makefile
@@ -33,7 +33,9 @@ API_TIMEOUT=30s
|
||||
API_ROOT_PATH=/
|
||||
API_TRACE_HEADER=Gotenberg-Trace
|
||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||
CHROMIUM_FAILED_STARTS_THRESHOLD=5
|
||||
CHROMIUM_RESTART_AFTER=0
|
||||
CHROMIUM_AUTO_START=false
|
||||
CHROMIUM_START_TIMEOUT=10s
|
||||
CHROMIUM_INCOGNITO=false
|
||||
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
||||
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||
@@ -80,7 +82,9 @@ run: ## Start a Gotenberg container
|
||||
--api-root-path=$(API_ROOT_PATH) \
|
||||
--api-trace-header=$(API_TRACE_HEADER) \
|
||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||
--chromium-failed-starts-threshold=$(CHROMIUM_FAILED_STARTS_THRESHOLD) \
|
||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
||||
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
||||
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
||||
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
||||
@@ -146,8 +150,9 @@ fmt: ## Format the code and "optimize" the dependencies
|
||||
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v7)" --skip-generated --skip-vendor --custom-order .
|
||||
go mod tidy
|
||||
|
||||
# go install golang.org/x/tools/cmd/godoc@latest
|
||||
.PHONY: godoc
|
||||
godoc: ## Run a webserver with Gotenberg godoc (go get golang.org/x/tools/cmd/godoc)
|
||||
godoc: ## Run a webserver with Gotenberg godoc
|
||||
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
|
||||
godoc -http=:6060
|
||||
|
||||
|
||||
23
go.mod
23
go.mod
@@ -3,21 +3,21 @@ module github.com/gotenberg/gotenberg/v7
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.7.0
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
|
||||
github.com/chromedp/chromedp v0.9.2
|
||||
github.com/alexliesenfeld/health v0.8.0
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20231019002500-864b42864d36
|
||||
github.com/chromedp/chromedp v0.9.3
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.4
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/compress v1.17.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/labstack/echo/v4 v4.11.2
|
||||
github.com/labstack/gommon v0.4.0
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.5.0
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
@@ -29,7 +29,7 @@ require (
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.16.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sync v0.4.0
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0
|
||||
@@ -45,7 +45,6 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||
@@ -53,10 +52,10 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
||||
59
go.sum
59
go.sum
@@ -1,19 +1,19 @@
|
||||
github.com/alexliesenfeld/health v0.7.0 h1:U3mSZ3ussRbGx+/rXBjNVxjLX5cKaNh8Ly9Hx3q+yfY=
|
||||
github.com/alexliesenfeld/health v0.7.0/go.mod h1:6Nnjbu7vBYHoZqIuZeOnTpnW7OH14ulR+wIBE2QuJ8I=
|
||||
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
||||
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617 h1:/5dwcyi5WOawM1Iz6MjrYqB90TRIdZv3O0fVHEJb86w=
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231019002500-864b42864d36 h1:bZQXbfLJ/7qq7CKZ7F1wgrY91SeBbuTcQtv6xjeHpMQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20231019002500-864b42864d36/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -26,13 +26,9 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@@ -57,8 +53,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
|
||||
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
@@ -67,8 +63,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
|
||||
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
|
||||
github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
|
||||
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
@@ -80,16 +76,16 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
@@ -108,8 +104,8 @@ github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -122,15 +118,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
@@ -152,9 +145,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -170,7 +162,6 @@ golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -80,6 +80,28 @@ func (f *ParsedFlags) MustDeprecatedBool(deprecated string, newName string) bool
|
||||
return f.MustBool(newName)
|
||||
}
|
||||
|
||||
// MustInt64 returns the int64 value of a flag given by name.
|
||||
// It panics if an error occurs.
|
||||
func (f *ParsedFlags) MustInt64(name string) int64 {
|
||||
val, err := f.GetInt64(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// MustDeprecatedInt64 returns the int64 value of a deprecated flag if it was
|
||||
// explicitly set or the int64 value of the new flag.
|
||||
// It panics if an error occurs.
|
||||
func (f *ParsedFlags) MustDeprecatedInt64(deprecated string, newName string) int64 {
|
||||
if f.Changed(deprecated) {
|
||||
return f.MustInt64(deprecated)
|
||||
}
|
||||
|
||||
return f.MustInt64(newName)
|
||||
}
|
||||
|
||||
// MustInt returns the int value of a flag given by name.
|
||||
// It panics if an error occurs.
|
||||
func (f *ParsedFlags) MustInt(name string) int {
|
||||
|
||||
@@ -252,6 +252,87 @@ func TestParsedFlags_MustDeprecatedBool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsedFlags_MustInt64(t *testing.T) {
|
||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||
fs.Int64("foo", 0, "")
|
||||
|
||||
err := fs.Parse([]string{"--foo=1"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||
|
||||
for i, tc := range []struct {
|
||||
name string
|
||||
expectPanic bool
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
},
|
||||
{
|
||||
name: "bar",
|
||||
expectPanic: true,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
if tc.expectPanic {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("test %d: expected panic but got none", i)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if !tc.expectPanic {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
parsedFlags.MustInt64(tc.name)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsedFlags_MustDeprecatedInt64(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
rawFlags []string
|
||||
expectValue int64
|
||||
}{
|
||||
{
|
||||
rawFlags: []string{"--foo=1"},
|
||||
expectValue: 1,
|
||||
},
|
||||
{
|
||||
rawFlags: []string{"--bar=2"},
|
||||
expectValue: 2,
|
||||
},
|
||||
{
|
||||
rawFlags: []string{"--foo=1", "--bar=2"},
|
||||
expectValue: 1,
|
||||
},
|
||||
} {
|
||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||
fs.Int64("foo", 0, "")
|
||||
fs.Int64("bar", 0, "")
|
||||
|
||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||
|
||||
err := parsedFlags.Parse(tc.rawFlags)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
actual := parsedFlags.MustDeprecatedInt64("foo", "bar")
|
||||
if actual != tc.expectValue {
|
||||
t.Errorf("test %d: expected %d but got %d", i, tc.expectValue, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsedFlags_MustInt(t *testing.T) {
|
||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||
fs.Int("foo", 0, "")
|
||||
|
||||
@@ -6,53 +6,106 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ModuleMock is a mock for the Module interface.
|
||||
// ModuleMock is a mock for the [Module] interface.
|
||||
type ModuleMock struct {
|
||||
DescriptorMock func() ModuleDescriptor
|
||||
}
|
||||
|
||||
func (mod ModuleMock) Descriptor() ModuleDescriptor {
|
||||
func (mod *ModuleMock) Descriptor() ModuleDescriptor {
|
||||
return mod.DescriptorMock()
|
||||
}
|
||||
|
||||
// ValidatorMock is a mock for the Validator interface.
|
||||
// ValidatorMock is a mock for the [Validator] interface.
|
||||
type ValidatorMock struct {
|
||||
ValidateMock func() error
|
||||
}
|
||||
|
||||
func (mod ValidatorMock) Validate() error {
|
||||
func (mod *ValidatorMock) Validate() error {
|
||||
return mod.ValidateMock()
|
||||
}
|
||||
|
||||
// PDFEngineMock is a mock for the PDFEngine interface.
|
||||
// PDFEngineMock is a mock for the [PDFEngine] interface.
|
||||
type PDFEngineMock struct {
|
||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
||||
}
|
||||
|
||||
func (engine PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
func (engine *PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
|
||||
}
|
||||
|
||||
func (engine PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
func (engine *PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
|
||||
}
|
||||
|
||||
// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
|
||||
// PDFEngineProviderMock is a mock for the [PDFEngineProvider] interface.
|
||||
type PDFEngineProviderMock struct {
|
||||
PDFEngineMock func() (PDFEngine, error)
|
||||
}
|
||||
|
||||
func (provider PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
|
||||
func (provider *PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
|
||||
return provider.PDFEngineMock()
|
||||
}
|
||||
|
||||
// LoggerProviderMock is a mock for the LoggerProvider interface.
|
||||
// ProcessMock is a mock for the [Process] interface.
|
||||
type ProcessMock struct {
|
||||
StartMock func(logger *zap.Logger) error
|
||||
StopMock func(logger *zap.Logger) error
|
||||
HealthyMock func(logger *zap.Logger) bool
|
||||
}
|
||||
|
||||
func (p *ProcessMock) Start(logger *zap.Logger) error {
|
||||
return p.StartMock(logger)
|
||||
}
|
||||
|
||||
func (p *ProcessMock) Stop(logger *zap.Logger) error {
|
||||
return p.StopMock(logger)
|
||||
}
|
||||
|
||||
func (p *ProcessMock) Healthy(logger *zap.Logger) bool {
|
||||
return p.HealthyMock(logger)
|
||||
}
|
||||
|
||||
// ProcessSupervisorMock is a mock for the [ProcessSupervisor] interface.
|
||||
type ProcessSupervisorMock struct {
|
||||
LaunchMock func() error
|
||||
ShutdownMock func() error
|
||||
HealthyMock func() bool
|
||||
RunMock func(ctx context.Context, logger *zap.Logger, task func() error) error
|
||||
ReqQueueSizeMock func() int64
|
||||
RestartsCountMock func() int64
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) Launch() error {
|
||||
return s.LaunchMock()
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) Shutdown() error {
|
||||
return s.ShutdownMock()
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) Healthy() bool {
|
||||
return s.HealthyMock()
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return s.RunMock(ctx, logger, task)
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) ReqQueueSize() int64 {
|
||||
return s.ReqQueueSizeMock()
|
||||
}
|
||||
|
||||
func (s *ProcessSupervisorMock) RestartsCount() int64 {
|
||||
return s.RestartsCountMock()
|
||||
}
|
||||
|
||||
// LoggerProviderMock is a mock for the [LoggerProvider] interface.
|
||||
type LoggerProviderMock struct {
|
||||
LoggerMock func(mod Module) (*zap.Logger, error)
|
||||
}
|
||||
|
||||
func (provider LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
|
||||
func (provider *LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
|
||||
return provider.LoggerMock(mod)
|
||||
}
|
||||
|
||||
@@ -62,5 +115,7 @@ var (
|
||||
_ Validator = (*ValidatorMock)(nil)
|
||||
_ PDFEngine = (*PDFEngineMock)(nil)
|
||||
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
|
||||
_ Process = (*ProcessMock)(nil)
|
||||
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
|
||||
_ LoggerProvider = (*LoggerProviderMock)(nil)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestModuleMock(t *testing.T) {
|
||||
mock := ModuleMock{
|
||||
mock := &ModuleMock{
|
||||
DescriptorMock: func() ModuleDescriptor {
|
||||
return ModuleDescriptor{ID: "foo", New: func() Module {
|
||||
return nil
|
||||
@@ -17,12 +17,12 @@ func TestModuleMock(t *testing.T) {
|
||||
}
|
||||
|
||||
if mock.Descriptor().ID != "foo" {
|
||||
t.Errorf("expected ID '%s' from mock.Descriptor(), but got '%s'", "foo", mock.Descriptor().ID)
|
||||
t.Errorf("expected ID '%s' from ModuleMock.Descriptor, but got '%s'", "foo", mock.Descriptor().ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorMock(t *testing.T) {
|
||||
mock := ValidatorMock{
|
||||
mock := &ValidatorMock{
|
||||
ValidateMock: func() error {
|
||||
return nil
|
||||
},
|
||||
@@ -30,12 +30,12 @@ func TestValidatorMock(t *testing.T) {
|
||||
|
||||
err := mock.Validate()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mock.Validate(), but got: %v", err)
|
||||
t.Errorf("expected no error from ValidatorMock.Validate, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineMock(t *testing.T) {
|
||||
mock := PDFEngineMock{
|
||||
mock := &PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -46,37 +46,119 @@ func TestPDFEngineMock(t *testing.T) {
|
||||
|
||||
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mock.Merge(), but got: %v", err)
|
||||
t.Errorf("expected no error from PDFEngineMock.Merge, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
|
||||
t.Errorf("expected no error from PDFEngineMock.Convert, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineProviderMock(t *testing.T) {
|
||||
mock := PDFEngineProviderMock{
|
||||
mock := &PDFEngineProviderMock{
|
||||
PDFEngineMock: func() (PDFEngine, error) {
|
||||
return PDFEngineMock{}, nil
|
||||
return new(PDFEngineMock), nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.PDFEngine()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mock.PDFEngine(), but got: %v", err)
|
||||
t.Errorf("expected no error from PDFEngineProviderMock.PDFEngine, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessMock(t *testing.T) {
|
||||
mock := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Start(zap.NewNop())
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessMock.Start, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Stop(zap.NewNop())
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessMock.Stop, but got: %v", err)
|
||||
}
|
||||
|
||||
healthy := mock.Healthy(zap.NewNop())
|
||||
if !healthy {
|
||||
t.Error("expected true from ProcessMock.Healthy, but got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisorMock(t *testing.T) {
|
||||
mock := &ProcessSupervisorMock{
|
||||
LaunchMock: func() error {
|
||||
return nil
|
||||
},
|
||||
ShutdownMock: func() error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func() bool {
|
||||
return true
|
||||
},
|
||||
RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return nil
|
||||
},
|
||||
ReqQueueSizeMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
RestartsCountMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Launch()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Launch, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Shutdown()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Shutdown, but got: %v", err)
|
||||
}
|
||||
|
||||
healthy := mock.Healthy()
|
||||
if !healthy {
|
||||
t.Error("expected true from ProcessSupervisorMock.Healthy, but got false")
|
||||
}
|
||||
|
||||
err = mock.Run(context.TODO(), zap.NewNop(), nil)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Run, but got: %v", err)
|
||||
}
|
||||
|
||||
size := mock.ReqQueueSize()
|
||||
if size != 0 {
|
||||
t.Errorf("expected 0 from ProcessSupervisorMock.ReqQueueSize, but got: %d", size)
|
||||
}
|
||||
|
||||
restarts := mock.RestartsCount()
|
||||
if restarts != 0 {
|
||||
t.Errorf("expected 0 from ProcessSupervisorMock.RestartsCount, but got: %d", restarts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerProviderMock(t *testing.T) {
|
||||
mock := LoggerProviderMock{
|
||||
mock := &LoggerProviderMock{
|
||||
LoggerMock: func(mod Module) (*zap.Logger, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Logger(ModuleMock{})
|
||||
_, err := mock.Logger(new(ModuleMock))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mock.Logger(), but got: %v", err)
|
||||
t.Errorf("expected no error from LoggerProviderMock.Logger, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
244
pkg/gotenberg/supervisor.go
Normal file
244
pkg/gotenberg/supervisor.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Process is an interface that represents an abstract process
|
||||
// and provides methods for starting, stopping, and checking the health of the
|
||||
// process.
|
||||
//
|
||||
// Implementations of this interface should handle the actual logic for
|
||||
// starting, stopping, and ensuring the process's health.
|
||||
type Process interface {
|
||||
// Start initiates the process and returns an error if the process cannot
|
||||
// be started.
|
||||
Start(logger *zap.Logger) error
|
||||
|
||||
// Stop terminates the process and returns an error if the process cannot
|
||||
// be stopped.
|
||||
Stop(logger *zap.Logger) error
|
||||
|
||||
// Healthy checks the health of the process. It returns true if the process
|
||||
// is healthy; otherwise, it returns false.
|
||||
Healthy(logger *zap.Logger) bool
|
||||
}
|
||||
|
||||
// ProcessSupervisor provides methods to manage a [Process], including
|
||||
// starting, stopping, and ensuring its health.
|
||||
//
|
||||
// Additionally, it allows for the execution of tasks while managing the
|
||||
// process's state and provides functionality for limiting the number of
|
||||
// requests that can be handled by the process, as well as managing a request
|
||||
// queue.
|
||||
type ProcessSupervisor interface {
|
||||
// Launch starts the managed [Process].
|
||||
Launch() error
|
||||
|
||||
// Shutdown stops the managed [Process].
|
||||
Shutdown() error
|
||||
|
||||
// Healthy checks and returns the health status of the managed [Process].
|
||||
//
|
||||
// If the process has not been started or is restarting, it is considered
|
||||
// healthy and true is returned. Otherwise, it returns the health status of
|
||||
// the actual process.
|
||||
Healthy() bool
|
||||
|
||||
// Run executes a provided task while managing the state of the [Process].
|
||||
//
|
||||
// Run manages the request queue and may restart the process if it is not
|
||||
// healthy or if the number of handled requests exceeds the maximum limit.
|
||||
//
|
||||
// It returns an error if the task cannot be run or if the process state
|
||||
// cannot be managed properly.
|
||||
Run(ctx context.Context, logger *zap.Logger, task func() error) error
|
||||
|
||||
// ReqQueueSize returns the current size of the request queue.
|
||||
ReqQueueSize() int64
|
||||
|
||||
// RestartsCount returns the current number of restart.
|
||||
RestartsCount() int64
|
||||
}
|
||||
|
||||
type processSupervisor struct {
|
||||
logger *zap.Logger
|
||||
process Process
|
||||
maxReqLimit int64
|
||||
mutexChan chan struct{}
|
||||
firstStart atomic.Bool
|
||||
reqCounter atomic.Int64
|
||||
reqQueueSize atomic.Int64
|
||||
restartsCounter atomic.Int64
|
||||
isRestarting atomic.Bool
|
||||
}
|
||||
|
||||
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
||||
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit int64) ProcessSupervisor {
|
||||
b := &processSupervisor{
|
||||
logger: logger,
|
||||
process: process,
|
||||
mutexChan: make(chan struct{}, 1),
|
||||
maxReqLimit: maxReqLimit,
|
||||
}
|
||||
b.reqCounter.Store(0)
|
||||
b.reqQueueSize.Store(0)
|
||||
b.restartsCounter.Store(0)
|
||||
b.isRestarting.Store(false)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *processSupervisor) Launch() error {
|
||||
s.logger.Debug("start process")
|
||||
err := s.process.Start(s.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start process: %w", err)
|
||||
}
|
||||
|
||||
s.firstStart.Store(true)
|
||||
s.logger.Debug("process successfully started")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *processSupervisor) Shutdown() error {
|
||||
s.logger.Debug("shutdown process")
|
||||
err := s.process.Stop(s.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutdown process: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("process successfully shutdown")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *processSupervisor) restart() error {
|
||||
if s.isRestarting.Load() {
|
||||
s.logger.Debug("process already restarting, skip restart")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Debug("restart process")
|
||||
s.isRestarting.Store(true)
|
||||
defer s.isRestarting.Store(false)
|
||||
|
||||
err := s.Shutdown()
|
||||
if err != nil {
|
||||
// No big deal? Chances are it's already stopped.
|
||||
s.logger.Debug(fmt.Sprintf("stop process before restart: %s", err))
|
||||
}
|
||||
|
||||
err = s.Launch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("restart process: %w", err)
|
||||
}
|
||||
|
||||
s.reqCounter.Store(0)
|
||||
s.restartsCounter.Add(1)
|
||||
s.logger.Debug("process successfully restarted")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *processSupervisor) Healthy() bool {
|
||||
if !s.firstStart.Load() {
|
||||
// A non-started process is always healthy.
|
||||
return true
|
||||
}
|
||||
|
||||
if s.isRestarting.Load() {
|
||||
// A restarting process is always healthy.
|
||||
return true
|
||||
}
|
||||
|
||||
return s.process.Healthy(s.logger)
|
||||
}
|
||||
|
||||
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
s.reqQueueSize.Add(1)
|
||||
|
||||
select {
|
||||
case s.mutexChan <- struct{}{}:
|
||||
logger.Debug("process lock acquired")
|
||||
s.reqQueueSize.Add(-1)
|
||||
s.reqCounter.Add(1)
|
||||
|
||||
defer func() {
|
||||
logger.Debug("process lock released")
|
||||
<-s.mutexChan
|
||||
}()
|
||||
|
||||
if !s.firstStart.Load() {
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.Launch()
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("process first start: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.Healthy() {
|
||||
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.restart()
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("process restart before task: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
||||
s.logger.Debug("max request limit reached, restarting...")
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.restart()
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("process restart before task: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: no error wrapping because it leaks on Chromium console exceptions output.
|
||||
return s.runWithDeadline(ctx, task)
|
||||
case <-ctx.Done():
|
||||
logger.Debug("failed to acquire process lock before deadline")
|
||||
s.reqQueueSize.Add(-1)
|
||||
|
||||
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *processSupervisor) runWithDeadline(ctx context.Context, task func() error) error {
|
||||
runChan := make(chan error, 1)
|
||||
go func() {
|
||||
runChan <- task()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-runChan:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *processSupervisor) ReqQueueSize() int64 {
|
||||
return s.reqQueueSize.Load()
|
||||
}
|
||||
|
||||
func (s *processSupervisor) RestartsCount() int64 {
|
||||
return s.restartsCounter.Load()
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ ProcessSupervisor = (*processSupervisor)(nil)
|
||||
)
|
||||
570
pkg/gotenberg/supervisor_test.go
Normal file
570
pkg/gotenberg/supervisor_test.go
Normal file
@@ -0,0 +1,570 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestProcessSupervisor_Launch(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
startError error
|
||||
expectError bool
|
||||
firstStartSet bool
|
||||
}{
|
||||
{
|
||||
scenario: "successful launch",
|
||||
startError: nil,
|
||||
expectError: false,
|
||||
firstStartSet: true,
|
||||
},
|
||||
{
|
||||
scenario: "failed launch",
|
||||
startError: errors.New("start error"),
|
||||
expectError: true,
|
||||
firstStartSet: false,
|
||||
},
|
||||
{
|
||||
scenario: "process already started",
|
||||
startError: nil,
|
||||
expectError: false,
|
||||
firstStartSet: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return tc.startError
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||
if tc.firstStartSet {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
|
||||
err := ps.Launch()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.firstStartSet && !ps.firstStart.Load() {
|
||||
t.Error("expected firstStart to be set but it was not")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_Shutdown(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
stopError error
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "successful shutdown",
|
||||
stopError: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "failed shutdown",
|
||||
stopError: errors.New("stop error"),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return tc.stopError
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5)
|
||||
err := ps.Shutdown()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_restart(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
initiallyRestarting bool
|
||||
startError error
|
||||
stopError error
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "already restarting",
|
||||
initiallyRestarting: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "successful restart",
|
||||
startError: nil,
|
||||
stopError: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "failed to stop during restart",
|
||||
startError: nil,
|
||||
stopError: errors.New("stop error"),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "failed to start during restart",
|
||||
startError: errors.New("start error"),
|
||||
stopError: nil,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return tc.startError
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return tc.stopError
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||
if tc.initiallyRestarting {
|
||||
ps.isRestarting.Store(true)
|
||||
}
|
||||
|
||||
err := ps.restart()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_Healthy(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
initiallyStarted bool
|
||||
initiallyRestarting bool
|
||||
processHealthy bool
|
||||
expectHealthy bool
|
||||
}{
|
||||
{
|
||||
scenario: "non-started process is always healthy",
|
||||
initiallyStarted: false,
|
||||
expectHealthy: true,
|
||||
},
|
||||
{
|
||||
scenario: "restarting process is always healthy",
|
||||
initiallyStarted: true,
|
||||
initiallyRestarting: true,
|
||||
expectHealthy: true,
|
||||
},
|
||||
{
|
||||
scenario: "process reports as healthy",
|
||||
initiallyStarted: true,
|
||||
processHealthy: true,
|
||||
expectHealthy: true,
|
||||
},
|
||||
{
|
||||
scenario: "process reports as unhealthy",
|
||||
initiallyStarted: true,
|
||||
processHealthy: false,
|
||||
expectHealthy: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return tc.processHealthy
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||
if tc.initiallyStarted {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
if tc.initiallyRestarting {
|
||||
ps.isRestarting.Store(true)
|
||||
}
|
||||
|
||||
healthy := ps.Healthy()
|
||||
|
||||
if healthy != tc.expectHealthy {
|
||||
t.Fatalf("expected healthy to be %v but got %v", tc.expectHealthy, healthy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_Run(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
initiallyStarted bool
|
||||
startError error
|
||||
processHealthy bool
|
||||
maxReqLimit int64
|
||||
tasksToRun int
|
||||
taskError error
|
||||
expectError bool
|
||||
expectedStartCalls int64
|
||||
expectedHealthyCalls int64
|
||||
expectedStopCalls int64
|
||||
}{
|
||||
{
|
||||
scenario: "successfully run task on non-started process",
|
||||
initiallyStarted: false,
|
||||
processHealthy: true,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 1,
|
||||
expectError: false,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 1,
|
||||
expectedStopCalls: 0,
|
||||
},
|
||||
{
|
||||
scenario: "cannot launch non-started process",
|
||||
initiallyStarted: false,
|
||||
startError: errors.New("launch error"),
|
||||
processHealthy: true,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 1,
|
||||
expectError: true,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 0,
|
||||
expectedStopCalls: 0,
|
||||
},
|
||||
{
|
||||
scenario: "run task with unhealthy process causing restart",
|
||||
initiallyStarted: true,
|
||||
processHealthy: false,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 1,
|
||||
expectError: false,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 1,
|
||||
expectedStopCalls: 1,
|
||||
},
|
||||
{
|
||||
scenario: "cannot restart unhealthy process",
|
||||
startError: errors.New("start error"),
|
||||
initiallyStarted: true,
|
||||
processHealthy: false,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 1,
|
||||
expectError: true,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 1,
|
||||
expectedStopCalls: 1,
|
||||
},
|
||||
{
|
||||
scenario: "run tasks reaching max request limit causing restart",
|
||||
initiallyStarted: true,
|
||||
processHealthy: true,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 3,
|
||||
expectError: false,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 3,
|
||||
expectedStopCalls: 1,
|
||||
},
|
||||
{
|
||||
scenario: "cannot restart after reaching max request limit",
|
||||
startError: errors.New("start error"),
|
||||
initiallyStarted: true,
|
||||
processHealthy: true,
|
||||
maxReqLimit: 2,
|
||||
tasksToRun: 2,
|
||||
expectError: true,
|
||||
expectedStartCalls: 1,
|
||||
expectedHealthyCalls: 2,
|
||||
expectedStopCalls: 1,
|
||||
},
|
||||
{
|
||||
scenario: "task error",
|
||||
initiallyStarted: true,
|
||||
processHealthy: true,
|
||||
maxReqLimit: 0,
|
||||
tasksToRun: 1,
|
||||
taskError: errors.New("task error"),
|
||||
expectError: true,
|
||||
expectedStartCalls: 0,
|
||||
expectedHealthyCalls: 1,
|
||||
expectedStopCalls: 0,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
var startCalls, healthyCalls, stopCalls atomic.Int64
|
||||
startCalls.Store(0)
|
||||
healthyCalls.Store(0)
|
||||
stopCalls.Store(0)
|
||||
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
startCalls.Add(1)
|
||||
return tc.startError
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
stopCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
healthyCalls.Add(1)
|
||||
return tc.processHealthy
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit).(*processSupervisor)
|
||||
if tc.initiallyStarted {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
|
||||
task := func() error {
|
||||
return tc.taskError
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errorChan := make(chan error, tc.tasksToRun)
|
||||
|
||||
for i := 0; i < tc.tasksToRun; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ps.Run(ctx, logger, task)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorChan)
|
||||
|
||||
for err := range errorChan {
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if startCalls.Load() != tc.expectedStartCalls {
|
||||
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
||||
}
|
||||
|
||||
if healthyCalls.Load() != tc.expectedHealthyCalls {
|
||||
t.Errorf("expected %d process.Healthy calls, got %d", tc.expectedHealthyCalls, healthyCalls.Load())
|
||||
}
|
||||
|
||||
if stopCalls.Load() != tc.expectedStopCalls {
|
||||
t.Errorf("expected %d process.Stop calls, got %d", tc.expectedStopCalls, stopCalls.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_runWithDeadline(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctxDone bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "task finished",
|
||||
ctxDone: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
ctxDone: true,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0).(*processSupervisor)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if tc.ctxDone {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := ps.runWithDeadline(ctx, func() error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
||||
|
||||
// Simulating a lock.
|
||||
ps.mutexChan <- struct{}{}
|
||||
|
||||
if ps.ReqQueueSize() != 0 {
|
||||
t.Fatalf("expected queue size to be 0 but got %d", ps.ReqQueueSize())
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errorChan := make(chan error, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ps.Run(ctx, logger, func() error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// We have to wait a little bit so that the request queue size may change.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
if ps.ReqQueueSize() != 10 {
|
||||
t.Fatalf("expected queue size to be 10 but got %d", ps.ReqQueueSize())
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorChan)
|
||||
|
||||
for err := range errorChan {
|
||||
if err == nil {
|
||||
t.Error("expected a lock error but got none")
|
||||
}
|
||||
}
|
||||
|
||||
if ps.ReqQueueSize() != 0 {
|
||||
t.Errorf("expected queue size to be 0 but got %d", ps.ReqQueueSize())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
initialRestartsCount int64
|
||||
restartAttempts int
|
||||
startError error
|
||||
stopError error
|
||||
expectedRestartsCount int64
|
||||
}{
|
||||
{
|
||||
scenario: "no restarts, counter remains 0",
|
||||
initialRestartsCount: 0,
|
||||
restartAttempts: 0,
|
||||
expectedRestartsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "successful restart increases counter",
|
||||
initialRestartsCount: 0,
|
||||
restartAttempts: 1,
|
||||
startError: nil,
|
||||
stopError: nil,
|
||||
expectedRestartsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "failed to stop during restart, no impact",
|
||||
initialRestartsCount: 0,
|
||||
restartAttempts: 1,
|
||||
startError: nil,
|
||||
stopError: errors.New("stop error"),
|
||||
expectedRestartsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "multiple successful restarts",
|
||||
initialRestartsCount: 0,
|
||||
restartAttempts: 3,
|
||||
startError: nil,
|
||||
stopError: nil,
|
||||
expectedRestartsCount: 3,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return tc.startError
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return tc.stopError
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
||||
ps.restartsCounter.Store(tc.initialRestartsCount)
|
||||
|
||||
for i := 0; i < tc.restartAttempts; i++ {
|
||||
_ = ps.restart()
|
||||
}
|
||||
|
||||
actualRestartsCount := ps.RestartsCount()
|
||||
if actualRestartsCount != tc.expectedRestartsCount {
|
||||
t.Fatalf("expected restarts count to be %d, but got %d", tc.expectedRestartsCount, actualRestartsCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,15 @@ func (ctx *ContextMock) SetDirPath(path string) {
|
||||
ctx.dirPath = path
|
||||
}
|
||||
|
||||
// DirPath returns the context's working directory path.
|
||||
//
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.SetDirPath("/foo")
|
||||
// dirPath := ctx.DirPath()
|
||||
func (ctx *ContextMock) DirPath() string {
|
||||
return ctx.dirPath
|
||||
}
|
||||
|
||||
// SetValues sets the values.
|
||||
//
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
|
||||
@@ -20,6 +20,18 @@ func TestContextMock_SetDirPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_DirPath(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetDirPath("/foo")
|
||||
|
||||
actual := mock.DirPath()
|
||||
expect := "/foo"
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetValues(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetValues(map[string][]string{
|
||||
|
||||
301
pkg/modules/chromium/browser.go
Normal file
301
pkg/modules/chromium/browser.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/fetch"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
type browser interface {
|
||||
gotenberg.Process
|
||||
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
||||
}
|
||||
|
||||
type browserArguments struct {
|
||||
// Executor args.
|
||||
binPath string
|
||||
incognito bool
|
||||
allowInsecureLocalhost bool
|
||||
ignoreCertificateErrors bool
|
||||
disableWebSecurity bool
|
||||
allowFileAccessFromFiles bool
|
||||
hostResolverRules string
|
||||
proxyServer string
|
||||
wsUrlReadTimeout time.Duration
|
||||
|
||||
// Tasks specific.
|
||||
allowList *regexp.Regexp
|
||||
denyList *regexp.Regexp
|
||||
disableJavaScript bool
|
||||
}
|
||||
|
||||
type chromiumBrowser struct {
|
||||
initialCtx context.Context
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
userProfileDirPath string
|
||||
ctxMu sync.RWMutex
|
||||
isStarted atomic.Bool
|
||||
|
||||
arguments browserArguments
|
||||
fs *gotenberg.FileSystem
|
||||
}
|
||||
|
||||
func newChromiumBrowser(arguments browserArguments) browser {
|
||||
b := &chromiumBrowser{
|
||||
initialCtx: context.Background(),
|
||||
arguments: arguments,
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
}
|
||||
b.isStarted.Store(false)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chromiumBrowser) Start(logger *zap.Logger) error {
|
||||
if b.isStarted.Load() {
|
||||
return errors.New("browser is already started")
|
||||
}
|
||||
|
||||
debug := &debugLogger{logger: logger}
|
||||
b.userProfileDirPath = b.fs.NewDirPath()
|
||||
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.CombinedOutput(debug),
|
||||
chromedp.ExecPath(b.arguments.binPath),
|
||||
chromedp.NoSandbox,
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/327
|
||||
// https://github.com/chromedp/chromedp/issues/904
|
||||
chromedp.DisableGPU,
|
||||
// See:
|
||||
// https://github.com/puppeteer/puppeteer/issues/661
|
||||
// https://github.com/puppeteer/puppeteer/issues/2410
|
||||
chromedp.Flag("font-render-hinting", "none"),
|
||||
chromedp.UserDataDir(b.userProfileDirPath),
|
||||
)
|
||||
|
||||
if b.arguments.incognito {
|
||||
opts = append(opts, chromedp.Flag("incognito", b.arguments.incognito))
|
||||
}
|
||||
|
||||
if b.arguments.allowInsecureLocalhost {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/488.
|
||||
opts = append(opts, chromedp.Flag("allow-insecure-localhost", true))
|
||||
}
|
||||
|
||||
if b.arguments.ignoreCertificateErrors {
|
||||
opts = append(opts, chromedp.IgnoreCertErrors)
|
||||
}
|
||||
|
||||
if b.arguments.disableWebSecurity {
|
||||
opts = append(opts, chromedp.Flag("disable-web-security", true))
|
||||
}
|
||||
|
||||
if b.arguments.allowFileAccessFromFiles {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/356.
|
||||
opts = append(opts, chromedp.Flag("allow-file-access-from-files", true))
|
||||
}
|
||||
|
||||
if b.arguments.hostResolverRules != "" {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/488.
|
||||
opts = append(opts, chromedp.Flag("host-resolver-rules", b.arguments.hostResolverRules))
|
||||
}
|
||||
|
||||
if b.arguments.proxyServer != "" {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/376.
|
||||
opts = append(opts, chromedp.ProxyServer(b.arguments.proxyServer))
|
||||
}
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/524.
|
||||
opts = append(opts, chromedp.WSURLReadTimeout(b.arguments.wsUrlReadTimeout))
|
||||
|
||||
allocatorCtx, allocatorCancel := chromedp.NewExecAllocator(b.initialCtx, opts...)
|
||||
ctx, cancel := chromedp.NewContext(allocatorCtx, chromedp.WithDebugf(debug.Printf))
|
||||
|
||||
err := chromedp.Run(ctx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
allocatorCancel()
|
||||
return fmt.Errorf("run exec allocator: %w", err)
|
||||
}
|
||||
|
||||
b.ctxMu.Lock()
|
||||
defer b.ctxMu.Unlock()
|
||||
|
||||
// We have to keep the context around, as we need it to create a new tabs
|
||||
// later.
|
||||
b.ctx = ctx
|
||||
b.cancelFunc = func() {
|
||||
cancel()
|
||||
allocatorCancel()
|
||||
}
|
||||
b.isStarted.Store(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
|
||||
if !b.isStarted.Load() {
|
||||
return errors.New("browser is already stopped")
|
||||
}
|
||||
|
||||
// Always remove the user profile directory created by Chromium.
|
||||
copyUserProfileDirPath := b.userProfileDirPath
|
||||
defer func(userProfileDirPath string) {
|
||||
go func() {
|
||||
// FIXME: Chromium seems to recreate the user profile directory
|
||||
// right after its deletion if we do not wait a certain amount
|
||||
// of time before re-deleting it.
|
||||
<-time.After(10 * time.Second)
|
||||
|
||||
err := os.RemoveAll(userProfileDirPath)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("remove Chromium's user profile directory: %s", err))
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
|
||||
}()
|
||||
}(copyUserProfileDirPath)
|
||||
|
||||
b.ctxMu.Lock()
|
||||
defer b.ctxMu.Unlock()
|
||||
|
||||
b.cancelFunc()
|
||||
b.ctx = nil
|
||||
b.userProfileDirPath = ""
|
||||
b.isStarted.Store(false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
|
||||
// Good to know: the supervisor does not call this method if no first start
|
||||
// or if the process is restarting.
|
||||
|
||||
if !b.isStarted.Load() {
|
||||
// Non-started browser but not restarting?
|
||||
return false
|
||||
}
|
||||
|
||||
b.ctxMu.RLock()
|
||||
defer b.ctxMu.RUnlock()
|
||||
|
||||
taskCtx, cancel := chromedp.NewContext(b.ctx)
|
||||
defer cancel()
|
||||
|
||||
err := chromedp.Run(taskCtx, chromedp.Navigate("about:blank"))
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("browser health check failed: %s", err))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
if !b.isStarted.Load() {
|
||||
return errors.New("browser not started, cannot handle PDF conversion")
|
||||
}
|
||||
|
||||
// We validate the "main" URL against our allow / deny lists.
|
||||
if !b.arguments.allowList.MatchString(url) {
|
||||
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
|
||||
}
|
||||
|
||||
if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
|
||||
return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
|
||||
}
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
return errors.New("context has no deadline")
|
||||
}
|
||||
|
||||
b.ctxMu.RLock()
|
||||
defer b.ctxMu.RUnlock()
|
||||
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Until(deadline))
|
||||
defer timeoutCancel()
|
||||
|
||||
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
|
||||
defer taskCancel()
|
||||
|
||||
// We validate all others requests against our allow / deny lists.
|
||||
// If a request does not pass the validation, we make it fail.
|
||||
listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
|
||||
|
||||
var (
|
||||
consoleExceptions error
|
||||
consoleExceptionsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||
if options.FailOnConsoleExceptions && !b.arguments.disableJavaScript {
|
||||
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||
}
|
||||
|
||||
tasks := chromedp.Tasks{
|
||||
network.Enable(),
|
||||
fetch.Enable(),
|
||||
runtime.Enable(),
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||
navigateActionFunc(logger, url),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||
forceExactColorsActionFunc(),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||
printToPdfActionFunc(logger, outputPath, options),
|
||||
}
|
||||
|
||||
err := chromedp.Run(taskCtx, tasks...)
|
||||
if err != nil {
|
||||
errMessage := err.Error()
|
||||
|
||||
if strings.Contains(errMessage, "Show invalid printer settings error (-32000)") || strings.Contains(errMessage, "content area is empty (-32602)") {
|
||||
return ErrInvalidPrinterSettings
|
||||
}
|
||||
|
||||
if strings.Contains(errMessage, "Page range syntax error") {
|
||||
return ErrPageRangesSyntaxError
|
||||
}
|
||||
|
||||
if strings.Contains(errMessage, "rpcc: message too large") {
|
||||
return ErrRpccMessageTooLarge
|
||||
}
|
||||
|
||||
return fmt.Errorf("print to PDF: %w", err)
|
||||
}
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||
consoleExceptionsMu.RLock()
|
||||
defer consoleExceptionsMu.RUnlock()
|
||||
|
||||
if consoleExceptions != nil {
|
||||
return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Process = (*chromiumBrowser)(nil)
|
||||
_ browser = (*chromiumBrowser)(nil)
|
||||
)
|
||||
1042
pkg/modules/chromium/browser_test.go
Normal file
1042
pkg/modules/chromium/browser_test.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,14 +14,6 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
type ProtoAPI struct {
|
||||
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error
|
||||
}
|
||||
|
||||
func (mod ProtoAPI) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
|
||||
return mod.pdf(ctx, logger, URL, outputPath, options)
|
||||
}
|
||||
|
||||
func TestDefaultOptions(t *testing.T) {
|
||||
actual := DefaultOptions()
|
||||
notExpect := Options{}
|
||||
@@ -33,7 +24,7 @@ func TestDefaultOptions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChromium_Descriptor(t *testing.T) {
|
||||
descriptor := Chromium{}.Descriptor()
|
||||
descriptor := new(Chromium).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Chromium))
|
||||
@@ -45,9 +36,9 @@ func TestChromium_Descriptor(t *testing.T) {
|
||||
|
||||
func TestChromium_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectErr bool
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no logger provider",
|
||||
@@ -59,12 +50,12 @@ func TestChromium_Provision(t *testing.T) {
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
)
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := struct {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -84,12 +75,12 @@ func TestChromium_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := struct {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -109,12 +100,12 @@ func TestChromium_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine from PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := struct {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
@@ -138,12 +129,12 @@ func TestChromium_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "provision success",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := struct {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
@@ -155,7 +146,7 @@ func TestChromium_Provision(t *testing.T) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
return gotenberg.PDFEngineMock{}, nil
|
||||
return new(gotenberg.PDFEngineMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
@@ -169,123 +160,233 @@ func TestChromium_Provision(t *testing.T) {
|
||||
}(),
|
||||
},
|
||||
} {
|
||||
mod := new(Chromium)
|
||||
err := mod.Provision(tc.ctx)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
err := mod.Provision(tc.ctx)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Validate(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
binPath string
|
||||
expectErr bool
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
expectErr: true,
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
binPath: "/foo",
|
||||
expectErr: true,
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
scenario: "valid bin path",
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
mod := new(Chromium)
|
||||
mod.binPath = tc.binPath
|
||||
err := mod.Validate()
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.args = browserArguments{
|
||||
binPath: tc.binPath,
|
||||
}
|
||||
err := mod.Validate()
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
}
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Start(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
autoStart bool
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no auto-start",
|
||||
autoStart: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start success",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start failed",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.autoStart = tc.autoStart
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
err := mod.Start()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_StartupMessage(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
|
||||
mod.autoStart = true
|
||||
autoStartMsg := mod.StartupMessage()
|
||||
|
||||
mod.autoStart = false
|
||||
noAutoStartMsg := mod.StartupMessage()
|
||||
|
||||
if autoStartMsg == noAutoStartMsg {
|
||||
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Stop(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "stop success",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "stop failed",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.logger = zap.NewNop()
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
|
||||
cancel()
|
||||
|
||||
err := mod.Stop(ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Metrics(t *testing.T) {
|
||||
metrics, err := new(Chromium).Metrics()
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||
ReqQueueSizeMock: func() int64 {
|
||||
return 10
|
||||
},
|
||||
RestartsCountMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
metrics, err := mod.Metrics()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if len(metrics) != 2 {
|
||||
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||
if len(metrics) != 4 {
|
||||
t.Fatalf("expected %d metrics, but got %d", 4, len(metrics))
|
||||
}
|
||||
|
||||
actual := metrics[0].Read()
|
||||
if actual != 0 {
|
||||
t.Errorf("expected %d Chromium instances, but got %f", 0, actual)
|
||||
if actual != float64(1) {
|
||||
t.Errorf("expected %f for chromium_active_instances_count, but got %f", float64(1), actual)
|
||||
}
|
||||
|
||||
actual = metrics[1].Read()
|
||||
if actual != 0 {
|
||||
t.Errorf("expected %d Chromium failed starts, but got %f", 0, actual)
|
||||
if actual != float64(0) {
|
||||
t.Errorf("expected %f for chromium_failed_starts_count, but got %f", float64(0), actual)
|
||||
}
|
||||
|
||||
actual = metrics[2].Read()
|
||||
if actual != float64(10) {
|
||||
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
|
||||
}
|
||||
|
||||
actual = metrics[3].Read()
|
||||
if actual != float64(0) {
|
||||
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Checks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mod Chromium
|
||||
tearUp func()
|
||||
tearDown func()
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
expectAvailabilityStatus health.AvailabilityStatus
|
||||
}{
|
||||
{
|
||||
name: "ignore Chromium failed starts",
|
||||
mod: Chromium{
|
||||
failedStartsThreshold: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with Chromium failed starts threshold not reached",
|
||||
mod: Chromium{
|
||||
failedStartsThreshold: 1,
|
||||
},
|
||||
scenario: "healthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return true
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusUp,
|
||||
},
|
||||
{
|
||||
name: "with Chromium failed starts threshold reached",
|
||||
mod: Chromium{
|
||||
failedStartsThreshold: 1,
|
||||
},
|
||||
tearUp: func() {
|
||||
failedStartsCount = 1
|
||||
},
|
||||
tearDown: func() {
|
||||
failedStartsCount = 0
|
||||
},
|
||||
scenario: "unhealthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return false
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusDown,
|
||||
},
|
||||
}
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.tearUp != nil {
|
||||
tc.tearUp()
|
||||
}
|
||||
|
||||
checks, err := tc.mod.Checks()
|
||||
checks, err := mod.Checks()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error from mod.Checks(), but got: %v", err)
|
||||
}
|
||||
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(checks) != 1 {
|
||||
t.Fatalf("expected 1 check from mod.Checks(), but got %d", len(checks))
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
checker := health.NewChecker(checks...)
|
||||
@@ -294,10 +395,6 @@ func TestChromium_Checks(t *testing.T) {
|
||||
if result.Status != tc.expectAvailabilityStatus {
|
||||
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||
}
|
||||
|
||||
if tc.tearDown != nil {
|
||||
tc.tearDown()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -312,412 +409,76 @@ func TestChromium_Chromium(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChromium_Routes(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
expectRoutes int
|
||||
disableRoutes bool
|
||||
}{
|
||||
{
|
||||
expectRoutes: 3,
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 3,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
scenario: "routes disabled",
|
||||
expectRoutes: 0,
|
||||
disableRoutes: true,
|
||||
},
|
||||
} {
|
||||
mod := new(Chromium)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
||||
}
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_PDF(t *testing.T) {
|
||||
func TestChromium_Pdf(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
timeout time.Duration
|
||||
cancel context.CancelFunc
|
||||
URL string
|
||||
options Options
|
||||
userAgent string
|
||||
incognito bool
|
||||
allowInsecureLocalhost bool
|
||||
ignoreCertificateErrors bool
|
||||
disableWebSecurity bool
|
||||
allowFileAccessFromFiles bool
|
||||
hostResolverRules string
|
||||
proxyServer string
|
||||
allowList *regexp.Regexp
|
||||
denyList *regexp.Regexp
|
||||
disableJavaScript bool
|
||||
expectErr bool
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
browser browser
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "context has no deadline",
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
|
||||
expectErr: true,
|
||||
scenario: "PDF task success",
|
||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "URL does not match the expression from the allowed list",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
allowList: regexp.MustCompile("file:///tmp/*"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "URL does not match the expression from the denied list",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
denyList: regexp.MustCompile("file:///tests/*"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with user agent",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
UserAgent: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail on console exceptions",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html",
|
||||
options: Options{
|
||||
FailOnConsoleExceptions: true,
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "disable JavaScript",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
|
||||
disableJavaScript: true,
|
||||
},
|
||||
{
|
||||
name: "with extra HTTP headers",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
ExtraHTTPHeaders: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with extra link tags",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
||||
options: Options{
|
||||
ExtraLinkTags: []LinkTag{
|
||||
{
|
||||
Href: "font.woff",
|
||||
},
|
||||
{
|
||||
Href: "style.css",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with invalid emulated media type",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||
options: Options{
|
||||
EmulatedMediaType: "foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with screen emulated media type",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||
options: Options{
|
||||
EmulatedMediaType: "screen",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with print emulated media type",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||
options: Options{
|
||||
EmulatedMediaType: "print",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with omit background but not print background",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
OmitBackground: true,
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with omit background and print background",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
OmitBackground: true,
|
||||
PrintBackground: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with extra script tags",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
||||
options: Options{
|
||||
ExtraScriptTags: []ScriptTag{
|
||||
{
|
||||
Src: "script.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with wait delay",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
WaitDelay: time.Duration(1) * time.Nanosecond,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with invalid wait window status",
|
||||
timeout: time.Duration(3) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||
options: Options{
|
||||
WaitWindowStatus: "foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with wait window status",
|
||||
timeout: time.Duration(3) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||
options: Options{
|
||||
WaitWindowStatus: "ready",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with wait for expression that should not happen",
|
||||
timeout: time.Duration(3) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||
options: Options{
|
||||
WaitForExpression: "window.status === 'foo'",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with valid wait for expression",
|
||||
timeout: time.Duration(3) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||
options: Options{
|
||||
WaitForExpression: "window.status === 'ready'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with invalid wait for expression",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
WaitForExpression: "return undefined",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with too big margin bottom",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
MarginBottom: 100,
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with invalid page ranges",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
PageRanges: "foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "with a lot of properties",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
userAgent: "foo",
|
||||
incognito: true,
|
||||
ignoreCertificateErrors: true,
|
||||
allowInsecureLocalhost: true,
|
||||
disableWebSecurity: true,
|
||||
allowFileAccessFromFiles: true,
|
||||
hostResolverRules: "foo",
|
||||
proxyServer: "foo",
|
||||
},
|
||||
{
|
||||
name: "with file using local and remote assets",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
|
||||
},
|
||||
{
|
||||
name: "URL does match the expression from the allowed list",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
|
||||
allowList: regexp.MustCompile("file:///tests/*"),
|
||||
},
|
||||
{
|
||||
name: "URL does match the expression from the denied list",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
|
||||
denyList: regexp.MustCompile("file:///etc/*"),
|
||||
},
|
||||
{
|
||||
name: "with custom header and footer templates",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
HeaderTemplate: func() string {
|
||||
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}(),
|
||||
FooterTemplate: func() string {
|
||||
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with custom header template only",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
HeaderTemplate: func() string {
|
||||
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}(),
|
||||
FooterTemplate: DefaultOptions().FooterTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with custom footer template only",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
HeaderTemplate: DefaultOptions().HeaderTemplate,
|
||||
FooterTemplate: func() string {
|
||||
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "without custom header and footer templates",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||
options: Options{
|
||||
HeaderTemplate: DefaultOptions().HeaderTemplate,
|
||||
FooterTemplate: DefaultOptions().FooterTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with file using a .gif",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html",
|
||||
},
|
||||
{
|
||||
name: "with allow file access from files",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample6/index.html",
|
||||
allowFileAccessFromFiles: true,
|
||||
},
|
||||
{
|
||||
name: "with file using a style attribute",
|
||||
timeout: time.Duration(60) * time.Second,
|
||||
URL: "file:///tests/test/testdata/chromium/html/sample7/index.html",
|
||||
scenario: "PDF task error",
|
||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return errors.New("PDF task error")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.binPath = os.Getenv("CHROMIUM_BIN_PATH")
|
||||
mod.userAgent = tc.userAgent
|
||||
mod.incognito = tc.incognito
|
||||
mod.allowInsecureLocalhost = tc.allowInsecureLocalhost
|
||||
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
|
||||
mod.disableWebSecurity = tc.disableWebSecurity
|
||||
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
|
||||
mod.hostResolverRules = tc.hostResolverRules
|
||||
mod.proxyServer = tc.proxyServer
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return task()
|
||||
}}
|
||||
mod.browser = tc.browser
|
||||
|
||||
if tc.allowList == nil {
|
||||
tc.allowList = regexp.MustCompile("")
|
||||
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.denyList == nil {
|
||||
tc.denyList = regexp.MustCompile("")
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
mod.allowList = tc.allowList
|
||||
mod.denyList = tc.denyList
|
||||
mod.disableJavaScript = tc.disableJavaScript
|
||||
mod.fs = gotenberg.NewFileSystem()
|
||||
|
||||
ctxFs := gotenberg.NewFileSystem()
|
||||
outputDir, err := ctxFs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("test %s: expected error but got: %v", tc.name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(ctxFs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("test %s: expected no error while cleaning up but got: %v", tc.name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if tc.timeout == 0 {
|
||||
err = mod.PDF(context.Background(), zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
||||
defer cancel()
|
||||
|
||||
err = mod.PDF(ctx, zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
|
||||
}
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %s: expected error but got: %v", tc.name, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %s: expected no error but got: %v", tc.name, err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ API = (*ProtoAPI)(nil)
|
||||
)
|
||||
|
||||
@@ -7,21 +7,21 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// debugLogger is wrapper around a zap.Logger which is used for debugging
|
||||
// debugLogger is wrapper around a [zap.Logger] which is used for debugging
|
||||
// Chromium.
|
||||
type debugLogger struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// Write logs the bytes in a debug message.
|
||||
func (debug debugLogger) Write(p []byte) (n int, err error) {
|
||||
func (debug *debugLogger) Write(p []byte) (n int, err error) {
|
||||
debug.logger.Debug(string(p))
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Printf logs a debug message.
|
||||
func (debug debugLogger) Printf(format string, v ...interface{}) {
|
||||
func (debug *debugLogger) Printf(format string, v ...interface{}) {
|
||||
debug.logger.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDebugLogger_Write(t *testing.T) {
|
||||
actual, err := debugLogger{logger: zap.NewNop()}.Write([]byte("foo"))
|
||||
actual, err := (&debugLogger{logger: zap.NewNop()}).Write([]byte("foo"))
|
||||
expected := len([]byte("foo"))
|
||||
|
||||
if actual != expected {
|
||||
@@ -20,5 +20,5 @@ func TestDebugLogger_Write(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDebugLogger_Printf(t *testing.T) {
|
||||
debugLogger{logger: zap.NewNop()}.Printf("%s", "foo")
|
||||
(&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package chromium provides a module which adds routes for converting HTML
|
||||
// documents to PDF. Other modules may also retrieve the API provided by this
|
||||
// documents to Pdf. Other modules may also retrieve the [Api] provided by this
|
||||
// module.
|
||||
package chromium
|
||||
|
||||
34
pkg/modules/chromium/mocks.go
Normal file
34
pkg/modules/chromium/mocks.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// ApiMock is a mock for the [Api] interface.
|
||||
type ApiMock struct {
|
||||
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
||||
}
|
||||
|
||||
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
|
||||
return api.PdfMock(ctx, logger, URL, outputPath, options)
|
||||
}
|
||||
|
||||
// browserMock is a mock for the [browser] interface.
|
||||
type browserMock struct {
|
||||
gotenberg.ProcessMock
|
||||
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
||||
}
|
||||
|
||||
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return b.pdfMock(ctx, logger, url, outputPath, options)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ Api = (*ApiMock)(nil)
|
||||
_ browser = (*browserMock)(nil)
|
||||
)
|
||||
34
pkg/modules/chromium/mocks_test.go
Normal file
34
pkg/modules/chromium/mocks_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestApiMock(t *testing.T) {
|
||||
mock := &ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserMock(t *testing.T) {
|
||||
mock := &browserMock{
|
||||
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||
)
|
||||
|
||||
// FormDataChromiumPDFOptions creates Options form the form data. Fallback to
|
||||
// FormDataChromiumPdfOptions creates [Options] from the form data. Fallback to
|
||||
// default value if the considered key is not present.
|
||||
func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
defaultOptions := DefaultOptions()
|
||||
|
||||
var (
|
||||
@@ -32,14 +32,14 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
waitWindowStatus string
|
||||
waitForExpression string
|
||||
userAgent string
|
||||
extraHTTPHeaders map[string]string
|
||||
extraHttpHeaders map[string]string
|
||||
emulatedMediaType string
|
||||
landscape, printBackground, omitBackground bool
|
||||
scale, paperWidth, paperHeight float64
|
||||
marginTop, marginBottom, marginLeft, marginRight float64
|
||||
pageRanges string
|
||||
headerTemplate, footerTemplate string
|
||||
preferCSSPageSize bool
|
||||
preferCssPageSize bool
|
||||
)
|
||||
|
||||
form := ctx.FormData().
|
||||
@@ -47,15 +47,15 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||
String("userAgent", &userAgent, defaultOptions.UserAgent).
|
||||
String("userAgent", &userAgent, ""). // FIXME: deprecated.
|
||||
Custom("extraHttpHeaders", func(value string) error {
|
||||
if value == "" {
|
||||
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders
|
||||
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &extraHTTPHeaders)
|
||||
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal extra HTTP headers: %w", err)
|
||||
}
|
||||
@@ -90,18 +90,26 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
|
||||
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
|
||||
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate).
|
||||
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
|
||||
Bool("preferCssPageSize", &preferCssPageSize, defaultOptions.PreferCssPageSize)
|
||||
|
||||
// FIXME: deprecated.
|
||||
if userAgent != "" {
|
||||
ctx.Log().Warn("'userAgent' is deprecated; prefer the 'extraHttpHeaders' form field instead")
|
||||
|
||||
if extraHttpHeaders == nil {
|
||||
extraHttpHeaders = make(map[string]string)
|
||||
}
|
||||
|
||||
extraHttpHeaders["User-Agent"] = userAgent
|
||||
}
|
||||
|
||||
options := Options{
|
||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||
WaitDelay: waitDelay,
|
||||
WaitWindowStatus: waitWindowStatus,
|
||||
WaitForExpression: waitForExpression,
|
||||
UserAgent: userAgent,
|
||||
ExtraHTTPHeaders: extraHTTPHeaders,
|
||||
ExtraLinkTags: defaultOptions.ExtraLinkTags,
|
||||
ExtraHttpHeaders: extraHttpHeaders,
|
||||
EmulatedMediaType: emulatedMediaType,
|
||||
ExtraScriptTags: defaultOptions.ExtraScriptTags,
|
||||
Landscape: landscape,
|
||||
PrintBackground: printBackground,
|
||||
OmitBackground: omitBackground,
|
||||
@@ -115,60 +123,36 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
PageRanges: pageRanges,
|
||||
HeaderTemplate: headerTemplate,
|
||||
FooterTemplate: footerTemplate,
|
||||
PreferCSSPageSize: preferCSSPageSize,
|
||||
PreferCssPageSize: preferCssPageSize,
|
||||
}
|
||||
|
||||
return form, options
|
||||
}
|
||||
|
||||
// convertURLRoute returns an api.Route which can convert a URL to PDF.
|
||||
func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
||||
func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/url",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPDFOptions(ctx)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
|
||||
var (
|
||||
URL string
|
||||
PDFformat string
|
||||
url string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
err := form.
|
||||
MandatoryString("url", &URL).
|
||||
String("pdfFormat", &PDFformat, "").
|
||||
Custom("extraLinkTags", func(value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &options.ExtraLinkTags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal extra link tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).
|
||||
Custom("extraScriptTags", func(value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &options.ExtraScriptTags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal extra script tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).
|
||||
MandatoryString("url", &url).
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||
}
|
||||
@@ -178,32 +162,33 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// convertHTMLRoute returns an api.Route which can convert an HTML file to PDF.
|
||||
func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
||||
// PDF.
|
||||
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/html",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPDFOptions(ctx)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
PDFformat string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
err := form.
|
||||
MandatoryPath("index.html", &inputPath).
|
||||
String("pdfFormat", &PDFformat, "").
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
URL := fmt.Sprintf("file://%s", inputPath)
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
|
||||
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||
}
|
||||
@@ -213,27 +198,27 @@ func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// convertMarkdownRoute returns an api.Route which can convert markdown files
|
||||
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
||||
// to PDF.
|
||||
func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/markdown",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPDFOptions(ctx)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
markdownPaths []string
|
||||
PDFformat string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
err := form.
|
||||
MandatoryPath("index.html", &inputPath).
|
||||
MandatoryPaths([]string{".md"}, &markdownPaths).
|
||||
String("pdfFormat", &PDFformat, "").
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -310,9 +295,9 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
return fmt.Errorf("write template result: %w", err)
|
||||
}
|
||||
|
||||
URL := fmt.Sprintf("file://%s", inputPath)
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
|
||||
err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||
}
|
||||
@@ -322,19 +307,19 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// convertURL is a stub which is called by the other methods of this file.
|
||||
func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL, PDFformat string, options Options) error {
|
||||
// convertUrl is a stub which is called by the other methods of this file.
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url, pdfFormat string, options Options) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err := chromium.PDF(ctx, ctx.Log(), URL, outputPath, options)
|
||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, ErrURLNotAuthorized) {
|
||||
if errors.Is(err, ErrUrlNotAuthorized) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert to PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusForbidden,
|
||||
fmt.Sprintf("'%s' does not match the authorized URLs", URL),
|
||||
fmt.Sprintf("'%s' does not match the authorized URLs", url),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -403,11 +388,11 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
|
||||
// Now, let's check if the client want to convert this result PDF
|
||||
// to a specific PDF format.
|
||||
|
||||
if PDFformat != "" {
|
||||
if pdfFormat != "" {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormat, convertInputPath, convertOutputPath)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
@@ -415,7 +400,7 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", pdfFormat),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
319
pkg/modules/chromium/tasks.go
Normal file
319
pkg/modules/chromium/tasks.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
printToPdf := page.PrintToPDF().
|
||||
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
||||
WithLandscape(options.Landscape).
|
||||
WithPrintBackground(options.PrintBackground).
|
||||
WithScale(options.Scale).
|
||||
WithPaperWidth(options.PaperWidth).
|
||||
WithPaperHeight(options.PaperHeight).
|
||||
WithMarginTop(options.MarginTop).
|
||||
WithMarginBottom(options.MarginBottom).
|
||||
WithMarginLeft(options.MarginLeft).
|
||||
WithMarginRight(options.MarginRight).
|
||||
WithPageRanges(options.PageRanges).
|
||||
WithPreferCSSPageSize(options.PreferCssPageSize)
|
||||
|
||||
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
|
||||
options.FooterTemplate != DefaultOptions().FooterTemplate
|
||||
|
||||
if !hasCustomHeaderFooter {
|
||||
logger.Debug("no custom header nor footer")
|
||||
|
||||
printToPdf = printToPdf.WithDisplayHeaderFooter(false)
|
||||
} else {
|
||||
logger.Debug("with custom header and/or footer")
|
||||
|
||||
printToPdf = printToPdf.
|
||||
WithDisplayHeaderFooter(true).
|
||||
WithHeaderTemplate(options.HeaderTemplate).
|
||||
WithFooterTemplate(options.FooterTemplate)
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("print to Pdf with: %+v", printToPdf))
|
||||
|
||||
_, stream, err := printToPdf.Do(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("print to Pdf: %w", err)
|
||||
}
|
||||
|
||||
reader := &streamReader{
|
||||
ctx: ctx,
|
||||
handle: stream,
|
||||
r: nil,
|
||||
pos: 0,
|
||||
eof: false,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := reader.Close()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("close reader: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open output path: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
buffer := bufio.NewReader(reader)
|
||||
|
||||
_, err = buffer.WriteTo(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write result to output path: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||
if !disable {
|
||||
logger.Debug("JavaScript not disabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("disable JavaScript")
|
||||
|
||||
err := emulation.SetScriptExecutionDisabled(true).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("disable JavaScript: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if len(extraHttpHeaders) == 0 {
|
||||
logger.Debug("no extra HTTP headers")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||
|
||||
headers := make(network.Headers, len(extraHttpHeaders))
|
||||
for key, value := range extraHttpHeaders {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func navigateActionFunc(logger *zap.Logger, url string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
logger.Debug(fmt.Sprintf("navigate to '%s'", url))
|
||||
|
||||
_, _, _, err := page.Navigate(url).Do(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("navigate to '%s': %w", url, err)
|
||||
}
|
||||
|
||||
err = runBatch(
|
||||
ctx,
|
||||
waitForEventDomContentEventFired(ctx, logger),
|
||||
waitForEventLoadEventFired(ctx, logger),
|
||||
waitForEventNetworkIdle(ctx, logger),
|
||||
waitForEventLoadingFinished(ctx, logger),
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("wait for events: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, printBackground bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/226.
|
||||
if !omitBackground {
|
||||
logger.Debug("default white background not hidden")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !printBackground {
|
||||
// See https://github.com/chromedp/chromedp/issues/1179#issuecomment-1284794416.
|
||||
return fmt.Errorf("validate omit background: %w", ErrOmitBackgroundWithoutPrintBackground)
|
||||
}
|
||||
|
||||
logger.Debug("hide default white background")
|
||||
|
||||
err := emulation.SetDefaultBackgroundColorOverride().WithColor(
|
||||
&cdp.RGBA{
|
||||
R: 0,
|
||||
G: 0,
|
||||
B: 0,
|
||||
A: 0,
|
||||
}).Do(ctx)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("hide default white background: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func forceExactColorsActionFunc() chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/354
|
||||
// https://github.com/puppeteer/puppeteer/issues/2685
|
||||
// https://github.com/chromedp/chromedp/issues/520
|
||||
script := `
|
||||
(() => {
|
||||
const css = 'html { -webkit-print-color-adjust: exact !important; }';
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
`
|
||||
|
||||
evaluate := chromedp.Evaluate(script, nil)
|
||||
err := evaluate.Do(ctx)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("add CSS for exact colors: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if mediaType == "" {
|
||||
logger.Debug("no emulated media type")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if mediaType != "screen" && mediaType != "print" {
|
||||
return fmt.Errorf("validate emulated media type '%s': %w", mediaType, ErrInvalidEmulatedMediaType)
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType))
|
||||
|
||||
emulatedMedia := emulation.SetEmulatedMedia()
|
||||
err := emulatedMedia.WithMedia(mediaType).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("emulate media type '%s': %w", mediaType, err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, delay time.Duration) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if disableJavaScript {
|
||||
logger.Debug("JavaScript disabled, skipping wait delay")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if delay <= 0 {
|
||||
logger.Debug("no wait delay")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We wait for a given amount of time so that JavaScript
|
||||
// scripts have a chance to finish before printing the page.
|
||||
logger.Debug(fmt.Sprintf("wait '%s' before print", delay))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("wait delay: %w", ctx.Err())
|
||||
case <-time.After(delay):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, expression string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if disableJavaScript {
|
||||
logger.Debug("JavaScript disabled, skipping wait expression")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if expression == "" {
|
||||
logger.Debug("no wait expression")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We wait until the evaluation of the expression is true or
|
||||
// until the context is done.
|
||||
logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression))
|
||||
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
|
||||
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
|
||||
case <-ticker.C:
|
||||
var ok bool
|
||||
|
||||
evaluate := chromedp.Evaluate(expression, &ok)
|
||||
err := evaluate.Do(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
|
||||
}
|
||||
|
||||
if ok {
|
||||
ticker.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
{
|
||||
name: "nominal behavior",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider1 := struct {
|
||||
provider1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
@@ -42,7 +42,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
return uno.APIMock{}, nil
|
||||
}
|
||||
|
||||
provider2 := struct {
|
||||
provider2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
}{}
|
||||
@@ -52,7 +52,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
}}
|
||||
}
|
||||
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
return gotenberg.PDFEngineMock{}, nil
|
||||
return &gotenberg.PDFEngineMock{}, nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
@@ -79,7 +79,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no API from UNO API provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
@@ -106,7 +106,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
@@ -133,7 +133,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no PDF engine from PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider1 := struct {
|
||||
provider1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
@@ -146,7 +146,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
return uno.APIMock{}, nil
|
||||
}
|
||||
|
||||
provider2 := struct {
|
||||
provider2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
}{}
|
||||
@@ -156,7 +156,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
}}
|
||||
}
|
||||
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
return gotenberg.PDFEngineMock{}, errors.New("foo")
|
||||
return &gotenberg.PDFEngineMock{}, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestUNO_Provider(t *testing.T) {
|
||||
{
|
||||
name: "nominal behavior",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
@@ -68,7 +68,7 @@ func TestUNO_Provider(t *testing.T) {
|
||||
{
|
||||
name: "no API from UNO API provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
uno.ProviderMock
|
||||
}{}
|
||||
|
||||
@@ -270,7 +270,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -304,7 +304,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -341,7 +341,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -381,7 +381,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -421,7 +421,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -461,7 +461,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -493,7 +493,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -526,7 +526,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -558,7 +558,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -590,7 +590,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestUNO_Provision(t *testing.T) {
|
||||
{
|
||||
name: "nominal behavior",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -61,7 +61,7 @@ func TestUNO_Provision(t *testing.T) {
|
||||
{
|
||||
name: "threshold from deprecated flag --unoconv-disable-listener",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -107,7 +107,7 @@ func TestUNO_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Package logging provides a module which creates a zap.Logger for other
|
||||
// Package logging provides a module which creates a [zap.Logger] for other
|
||||
// modules.
|
||||
package logging
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
gotenberg.MustRegisterModule(Logging{})
|
||||
gotenberg.MustRegisterModule(new(Logging))
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -31,15 +31,16 @@ const (
|
||||
textLoggingFormat = "text"
|
||||
)
|
||||
|
||||
// Logging is a module which implements the gotenberg.LoggerProvider interface.
|
||||
// Logging is a module which implements the [gotenberg.LoggerProvider]
|
||||
// interface.
|
||||
type Logging struct {
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
}
|
||||
|
||||
// Descriptor returns a Logging's module descriptor.
|
||||
func (Logging) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// Descriptor returns a [Logging]'s module descriptor.
|
||||
func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
ID: "logging",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
@@ -66,7 +67,7 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
|
||||
}
|
||||
|
||||
// Validate validates the log level and format.
|
||||
func (log Logging) Validate() error {
|
||||
func (log *Logging) Validate() error {
|
||||
var err error
|
||||
|
||||
switch log.level {
|
||||
@@ -92,8 +93,8 @@ func (log Logging) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logger returns a zap.Logger.
|
||||
func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
// Logger returns a [zap.Logger].
|
||||
func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
if logger == nil {
|
||||
lvl, err := newLogLevel(log.level)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestLogging_Descriptor(t *testing.T) {
|
||||
descriptor := Logging{}.Descriptor()
|
||||
descriptor := new(Logging).Descriptor()
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Logging))
|
||||
|
||||
@@ -56,66 +56,68 @@ func TestLogging_Provision(t *testing.T) {
|
||||
expectFieldsPrefix: "",
|
||||
},
|
||||
} {
|
||||
var flags []string
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
var flags []string
|
||||
|
||||
if tc.level != "" {
|
||||
flags = append(flags, "--log-level", tc.level)
|
||||
}
|
||||
if tc.level != "" {
|
||||
flags = append(flags, "--log-level", tc.level)
|
||||
}
|
||||
|
||||
if tc.format != "" {
|
||||
flags = append(flags, "--log-format", tc.format)
|
||||
}
|
||||
if tc.format != "" {
|
||||
flags = append(flags, "--log-format", tc.format)
|
||||
}
|
||||
|
||||
if tc.fieldsPrefix != "" {
|
||||
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
|
||||
}
|
||||
if tc.fieldsPrefix != "" {
|
||||
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
|
||||
}
|
||||
|
||||
logging := new(Logging)
|
||||
fs := logging.Descriptor().FlagSet
|
||||
logging := new(Logging)
|
||||
fs := logging.Descriptor().FlagSet
|
||||
|
||||
err := fs.Parse(flags)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
err := fs.Parse(flags)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while parsing flags but got: %v", err)
|
||||
}
|
||||
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
|
||||
|
||||
err = logging.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
err = logging.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while provisioning but got: %v", err)
|
||||
}
|
||||
|
||||
if logging.level != tc.expectLevel {
|
||||
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectLevel, logging.level)
|
||||
}
|
||||
if logging.level != tc.expectLevel {
|
||||
t.Errorf("expected logging level '%s' but got '%s'", tc.expectLevel, logging.level)
|
||||
}
|
||||
|
||||
if logging.format != tc.expectFormat {
|
||||
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFormat, logging.format)
|
||||
}
|
||||
if logging.format != tc.expectFormat {
|
||||
t.Errorf("expected logging format '%s' but got '%s'", tc.expectFormat, logging.format)
|
||||
}
|
||||
|
||||
if logging.fieldsPrefix != tc.expectFieldsPrefix {
|
||||
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFieldsPrefix, logging.fieldsPrefix)
|
||||
}
|
||||
if logging.fieldsPrefix != tc.expectFieldsPrefix {
|
||||
t.Errorf("expected logging fields prefix '%s' but got '%s'", tc.expectFieldsPrefix, logging.fieldsPrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogging_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level string
|
||||
format string
|
||||
expectErr bool
|
||||
scenario string
|
||||
level string
|
||||
format string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectErr: true,
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectErr: true,
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid level and format",
|
||||
@@ -129,11 +131,11 @@ func TestLogging_Validate(t *testing.T) {
|
||||
|
||||
err := logging.Validate()
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
if tc.expectError && err == nil {
|
||||
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
if !tc.expectError && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
}
|
||||
@@ -145,18 +147,18 @@ func TestLogging_Logger(t *testing.T) {
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectErr: true,
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectErr: true,
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid level and format",
|
||||
@@ -164,24 +166,26 @@ func TestLogging_Logger(t *testing.T) {
|
||||
format: autoLoggingFormat,
|
||||
},
|
||||
} {
|
||||
logging := new(Logging)
|
||||
logging.level = tc.level
|
||||
logging.format = tc.format
|
||||
logging.fieldsPrefix = tc.fieldsPrefix
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logging := new(Logging)
|
||||
logging.level = tc.level
|
||||
logging.format = tc.format
|
||||
logging.fieldsPrefix = tc.fieldsPrefix
|
||||
|
||||
_, err := logging.Logger(gotenberg.ModuleMock{
|
||||
DescriptorMock: func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
|
||||
},
|
||||
_, err := logging.Logger(&gotenberg.ModuleMock{
|
||||
DescriptorMock: func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
|
||||
},
|
||||
})
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,44 +212,46 @@ func TestCustomCore(t *testing.T) {
|
||||
level: zapcore.ErrorLevel,
|
||||
},
|
||||
} {
|
||||
core, obsvr := observer.New(tc.level)
|
||||
lgr := zap.New(customCore{
|
||||
Core: core,
|
||||
fieldsPrefix: tc.fieldsPrefix,
|
||||
}).With(zap.String("a_field", "a value"))
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
core, obsvr := observer.New(tc.level)
|
||||
lgr := zap.New(customCore{
|
||||
Core: core,
|
||||
fieldsPrefix: tc.fieldsPrefix,
|
||||
}).With(zap.String("a_field", "a value"))
|
||||
|
||||
lgr.Debug("a debug message", zap.String("another_field", "another value"))
|
||||
lgr.Debug("a debug message", zap.String("another_field", "another value"))
|
||||
|
||||
entries := obsvr.TakeAll()
|
||||
entries := obsvr.TakeAll()
|
||||
|
||||
if tc.expectEntry && len(entries) == 0 {
|
||||
t.Fatalf("%s: expected an entry", tc.scenario)
|
||||
}
|
||||
|
||||
if !tc.expectEntry && len(entries) != 0 {
|
||||
t.Fatalf("%s: expected no entry", tc.scenario)
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if tc.fieldsPrefix != "" {
|
||||
prefix = tc.fieldsPrefix + "_"
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
fields := entry.Context
|
||||
|
||||
if len(fields) != 2 {
|
||||
t.Fatalf("expected 2 fields but got %d", len(fields))
|
||||
if tc.expectEntry && len(entries) == 0 {
|
||||
t.Fatal("expected an entry")
|
||||
}
|
||||
|
||||
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
|
||||
if !tc.expectEntry && len(entries) != 0 {
|
||||
t.Fatal("expected no entry")
|
||||
}
|
||||
|
||||
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
|
||||
var prefix string
|
||||
if tc.fieldsPrefix != "" {
|
||||
prefix = tc.fieldsPrefix + "_"
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
fields := entry.Context
|
||||
|
||||
if len(fields) != 2 {
|
||||
t.Fatalf("expected 2 fields but got %d", len(fields))
|
||||
}
|
||||
|
||||
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
|
||||
}
|
||||
|
||||
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +260,7 @@ func Test_newLogLevel(t *testing.T) {
|
||||
scenario string
|
||||
level string
|
||||
expectZapLevel zapcore.Level
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "error level",
|
||||
@@ -280,30 +286,32 @@ func Test_newLogLevel(t *testing.T) {
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectZapLevel: zapcore.InvalidLevel,
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
actual, err := newLogLevel(tc.level)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
actual, err := newLogLevel(tc.level)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectZapLevel != actual {
|
||||
t.Errorf("%s: expected %d level but got %d", tc.scenario, tc.expectZapLevel, actual)
|
||||
}
|
||||
if tc.expectZapLevel != actual {
|
||||
t.Errorf("expected %d level but got %d", tc.expectZapLevel, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newLogEncoder(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
format string
|
||||
expectErr bool
|
||||
scenario string
|
||||
format string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "auto format",
|
||||
@@ -318,19 +326,21 @@ func Test_newLogEncoder(t *testing.T) {
|
||||
format: jsonLoggingFormat,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
format: "foo",
|
||||
expectErr: true,
|
||||
scenario: "invalid format",
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
_, err := newLogEncoder(tc.format)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
_, err := newLogEncoder(tc.format)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
if !tc.expectError && err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge merges the given PDFs into a unique PDF.
|
||||
// Merge merges the given PDFs into a unique Pdf.
|
||||
func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
|
||||
if err == nil {
|
||||
@@ -50,9 +50,9 @@ func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string
|
||||
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
|
||||
}
|
||||
|
||||
// Convert is not available for this PDF engine.
|
||||
// Convert is not available for this Pdf engine.
|
||||
func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||
return fmt.Errorf("convert PDF to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
return fmt.Errorf("convert Pdf to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
{
|
||||
name: "nominal behavior",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -31,12 +31,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
{
|
||||
name: "at least one engine does not return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -47,12 +47,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
{
|
||||
name: "all engines return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -64,7 +64,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
{
|
||||
name: "context expired",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -105,7 +105,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
{
|
||||
name: "nominal behavior",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -116,12 +116,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
{
|
||||
name: "at least one engine does not return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -132,12 +132,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
{
|
||||
name: "all engines return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -149,7 +149,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
{
|
||||
name: "context expired",
|
||||
engine: newMultiPDFEngines(
|
||||
gotenberg.PDFEngineMock{
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -45,7 +45,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
engine := struct {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
@@ -72,7 +72,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
{
|
||||
name: "selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -85,7 +85,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
engine1 := struct {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
@@ -97,7 +97,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine2 := struct {
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
@@ -131,7 +131,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
{
|
||||
name: "user select deprecated unoconv-pdfengine",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -144,7 +144,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
engine := struct {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
@@ -189,7 +189,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -216,7 +216,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
{
|
||||
name: "no valid PDF engines",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := struct {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
@@ -229,7 +229,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
engine := struct {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
@@ -292,7 +292,7 @@ func TestPDFEngines_Validate(t *testing.T) {
|
||||
name: "existing PDF engine",
|
||||
names: []string{"foo"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engine := struct {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
}{}
|
||||
@@ -309,7 +309,7 @@ func TestPDFEngines_Validate(t *testing.T) {
|
||||
name: "non-existing bar PDF engine",
|
||||
names: []string{"foo", "bar", "baz"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engine1 := struct {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
}{}
|
||||
@@ -317,7 +317,7 @@ func TestPDFEngines_Validate(t *testing.T) {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
}
|
||||
|
||||
engine2 := struct {
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
}{}
|
||||
@@ -377,7 +377,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||
mod := PDFEngines{
|
||||
names: []string{"foo", "bar"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engine1 := struct {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
}{}
|
||||
@@ -385,7 +385,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
}
|
||||
|
||||
engine2 := struct {
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
}{}
|
||||
@@ -416,7 +416,7 @@ func TestPDFEngines_Routes(t *testing.T) {
|
||||
name: "route not disabled",
|
||||
mod: PDFEngines{
|
||||
engines: []gotenberg.PDFEngine{
|
||||
gotenberg.PDFEngineMock{},
|
||||
&gotenberg.PDFEngineMock{},
|
||||
},
|
||||
},
|
||||
expectRoutesCount: 2,
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -57,7 +57,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -79,7 +79,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -104,7 +104,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -129,7 +129,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -152,7 +152,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -226,7 +226,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -250,7 +250,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -302,7 +302,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -324,7 +324,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
},
|
||||
@@ -349,7 +349,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
91
test/testdata/chromium/html/index.html
vendored
Normal file
91
test/testdata/chromium/html/index.html
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
<style>
|
||||
@media print {
|
||||
#screen { display: none }
|
||||
}
|
||||
@media screen {
|
||||
#print { display: none }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-break-after">
|
||||
<div class="center">
|
||||
<h1>Gutenberg</h1>
|
||||
<img src="img.gif" alt="An image">
|
||||
</div>
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
<h2>This paragraph uses the default font</h2>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph uses a Google font</h2>
|
||||
<p class="google-font">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph uses a local font</h2>
|
||||
<p class="local-font">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
|
||||
<div class="center page-break-after">
|
||||
<h1>This image is loaded from a URL</h1>
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png">
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
<h2>This paragraph appears if wait delay > 2 seconds or if expression window.globalVar === 'ready' returns true</h2>
|
||||
<p id="wait" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph appears if the emulated media type is 'print'</h2>
|
||||
<p id="print">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph appears if the emulated media type is 'screen'</h2>
|
||||
<p id="screen">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
<h2>This paragraph appears if JavaScript is NOT disabled</h2>
|
||||
<p id="javascript" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
|
||||
<script type="application/javascript">
|
||||
var globalVar = 'notReady'
|
||||
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms))
|
||||
delay(2000).then(() => {
|
||||
document.getElementById('wait').style.display = 'contents'
|
||||
window.globalVar = 'ready'
|
||||
})
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
document.getElementById('javascript').style.display = 'contents'
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
console.log("a simple message")
|
||||
console.debug("a debug message")
|
||||
console.warn("a warning message")
|
||||
console.error("an error message")
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
throw new Error("Exception 1")
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
throw new Error("Exception 2")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
38
test/testdata/chromium/html/sample1/index.html
vendored
38
test/testdata/chromium/html/sample1/index.html
vendored
@@ -1,38 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-break-after">
|
||||
<div class="center">
|
||||
<h1>Gutenberg</h1>
|
||||
<img src="img.gif">
|
||||
</div>
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
<h2>This paragraph use the default font</h2>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph use a Google font</h2>
|
||||
<p class="google-font">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
|
||||
<h2>This paragraph use a local font</h2>
|
||||
<p class="local-font">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<h1>This image is loaded from a URL</h1>
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
24
test/testdata/chromium/html/sample10/index.html
vendored
24
test/testdata/chromium/html/sample10/index.html
vendored
@@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Console API</p>
|
||||
|
||||
<script type="application/javascript">
|
||||
console.log("a simple message")
|
||||
console.debug("a debug message")
|
||||
console.warn("a warning message")
|
||||
console.error("an error message")
|
||||
</script>
|
||||
<script type="application/javascript">
|
||||
throw new Error("Exception 1")
|
||||
</script>
|
||||
<script type="application/javascript">
|
||||
throw new Error("Exception 2")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
test/testdata/chromium/html/sample2/index.html
vendored
28
test/testdata/chromium/html/sample2/index.html
vendored
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p id="status">
|
||||
No window.status
|
||||
</p>
|
||||
|
||||
<script type="application/javascript">
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
const changeStatus = async (status) => {
|
||||
await delay(2000)
|
||||
console.log('waited 2s')
|
||||
|
||||
document.getElementById('status').innerText = 'window.status === ' + status
|
||||
|
||||
window.status = status
|
||||
};
|
||||
|
||||
changeStatus('ready')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
test/testdata/chromium/html/sample3/index.html
vendored
10
test/testdata/chromium/html/sample3/index.html
vendored
@@ -1,10 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src='file:///etc/passwd'></iframe>
|
||||
</body>
|
||||
</html>
|
||||
10
test/testdata/chromium/html/sample4/index.html
vendored
10
test/testdata/chromium/html/sample4/index.html
vendored
@@ -1,10 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, world!</p>
|
||||
</body>
|
||||
</html>
|
||||
BIN
test/testdata/chromium/html/sample5/MyImg.gif
vendored
BIN
test/testdata/chromium/html/sample5/MyImg.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
11
test/testdata/chromium/html/sample5/index.html
vendored
11
test/testdata/chromium/html/sample5/index.html
vendored
@@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Gutenberg</h1>
|
||||
<img src="MyImg.gif">
|
||||
</body>
|
||||
</html>
|
||||
20
test/testdata/chromium/html/sample6/index.html
vendored
20
test/testdata/chromium/html/sample6/index.html
vendored
@@ -1,20 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="center">
|
||||
<h1>Gutenberg</h1>
|
||||
</div>
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<script src="paged.polyfill.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
32782
test/testdata/chromium/html/sample6/paged.polyfill.js
vendored
32782
test/testdata/chromium/html/sample6/paged.polyfill.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
14
test/testdata/chromium/html/sample7/index.html
vendored
14
test/testdata/chromium/html/sample7/index.html
vendored
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Gutenberg</h1>
|
||||
<p style="color: #d1d0d0;">
|
||||
It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
19
test/testdata/chromium/html/sample8/index.html
vendored
19
test/testdata/chromium/html/sample8/index.html
vendored
@@ -1,19 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
<style>
|
||||
@media print {
|
||||
#screen { display: none }
|
||||
}
|
||||
@media screen {
|
||||
#print { display: none }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p id="print">Print media type</p>
|
||||
<p id="screen">Screen media type</p>
|
||||
</body>
|
||||
</html>
|
||||
17
test/testdata/chromium/html/sample9/index.html
vendored
17
test/testdata/chromium/html/sample9/index.html
vendored
@@ -1,17 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p id="javascript">
|
||||
JavaScript disabled.
|
||||
</p>
|
||||
|
||||
<script type="application/javascript">
|
||||
document.getElementById('javascript').innerText = 'JavaScript not disabled.'
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,4 +25,5 @@ body {
|
||||
.page-break-after {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -20,15 +20,24 @@
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
{{ toHTML "markdown1.md" }}
|
||||
{{ toHTML "defaultfont.md" }}
|
||||
|
||||
<div class="google-font">
|
||||
{{ toHTML "markdown2.md" }}
|
||||
{{ toHTML "googlefont.md" }}
|
||||
</div>
|
||||
|
||||
<div class="local-font">
|
||||
{{ toHTML "markdown3.md" }}
|
||||
{{ toHTML "localfont.md" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
{{ toHTML "table.md" }}
|
||||
|
||||
<h2>HTML from previous table</h2>
|
||||
<textarea readonly="readonly" cols="100" rows="50">
|
||||
{{ toHTML "table.md" }}
|
||||
</textarea>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-break-after">
|
||||
<div class="center">
|
||||
<h1>Gutenberg</h1>
|
||||
</div>
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div class="page-break-after">
|
||||
{{ toHTML "markdown.md" }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gutenberg</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Table in markdown</h1>
|
||||
|
||||
<h2>toHTML</h2>
|
||||
{{ toHTML "markdown.md" }}
|
||||
|
||||
<h2>Actual HTML</h2>
|
||||
<textarea readonly="readonly" cols="100" rows="50">
|
||||
{{ toHTML "markdown.md" }}
|
||||
</textarea>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
## This paragraph displays a table from a markdown file
|
||||
|
||||
| Tables | Are | Cool |
|
||||
|----------|:-------------:|------:|
|
||||
| col 1 is | left-aligned | $1600 |
|
||||
15
test/testdata/chromium/url/sample1/footer.html
vendored
15
test/testdata/chromium/url/sample1/footer.html
vendored
@@ -1,15 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
13
test/testdata/chromium/url/sample1/header.html
vendored
13
test/testdata/chromium/url/sample1/header.html
vendored
@@ -1,13 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span class="title"></span>
|
||||
</body>
|
||||
</html>
|
||||
8206
test/testdata/chromium/url/sample2/footer.html
vendored
8206
test/testdata/chromium/url/sample2/footer.html
vendored
File diff suppressed because it is too large
Load Diff
8206
test/testdata/chromium/url/sample2/header.html
vendored
8206
test/testdata/chromium/url/sample2/header.html
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user