fix: chromium memory leaks (#705)

This commit is contained in:
Julien Neuhart
2023-10-23 17:52:30 +02:00
committed by GitHub
parent b5a59e4de0
commit 54daac329e
71 changed files with 4248 additions and 51761 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/coverage.html /coverage.html
/coverage.txt /coverage.txt
/TODO.txt

View File

@@ -1,18 +1,11 @@
linters-settings: linters-settings:
gci: gci:
sections: sections:
- standard # Standard section: captures all standard packages. - standard
- default # Default section: contains all imports that could not be matched to another section type. - default
- prefix(github.com/gotenberg/gotenberg/v7) # Ensure that this is always at the top and always has a line break. - prefix(github.com/gotenberg/gotenberg/v7)
# Skip generated files.
# Default: true
skip-generated: true skip-generated: true
# Skip vendor files.
# Default: true
skip-vendor: 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 custom-order: true
linters: linters:

View File

@@ -33,7 +33,9 @@ API_TIMEOUT=30s
API_ROOT_PATH=/ API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace API_TRACE_HEADER=Gotenberg-Trace
API_DISABLE_HEALTH_CHECK_LOGGING=false 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_INCOGNITO=false
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
@@ -80,7 +82,9 @@ run: ## Start a Gotenberg container
--api-root-path=$(API_ROOT_PATH) \ --api-root-path=$(API_ROOT_PATH) \
--api-trace-header=$(API_TRACE_HEADER) \ --api-trace-header=$(API_TRACE_HEADER) \
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \ --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-incognito=$(CHROMIUM_INCOGNITO) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \ --chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \ --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 . gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v7)" --skip-generated --skip-vendor --custom-order .
go mod tidy go mod tidy
# go install golang.org/x/tools/cmd/godoc@latest
.PHONY: godoc .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) $(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
godoc -http=:6060 godoc -http=:6060

23
go.mod
View File

@@ -3,21 +3,21 @@ module github.com/gotenberg/gotenberg/v7
go 1.21 go 1.21
require ( require (
github.com/alexliesenfeld/health v0.7.0 github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617 github.com/chromedp/cdproto v0.0.0-20231019002500-864b42864d36
github.com/chromedp/chromedp v0.9.2 github.com/chromedp/chromedp v0.9.3
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.1 github.com/google/uuid v1.3.1
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 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/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/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/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/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.5.0 github.com/pdfcpu/pdfcpu v0.5.0
github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect
@@ -29,7 +29,7 @@ require (
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/crypto v0.14.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/image v0.13.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/sync v0.4.0
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 golang.org/x/term v0.13.0
@@ -45,7 +45,6 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.0 // 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/gorilla/css v1.0.0 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/tiff v1.0.1 // indirect github.com/hhrutter/tiff v1.0.1 // indirect
@@ -53,10 +52,10 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // 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/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.5.0 // 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/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect

59
go.sum
View File

@@ -1,19 +1,19 @@
github.com/alexliesenfeld/health v0.7.0 h1:U3mSZ3ussRbGx+/rXBjNVxjLX5cKaNh8Ly9Hx3q+yfY= github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
github.com/alexliesenfeld/health v0.7.0/go.mod h1:6Nnjbu7vBYHoZqIuZeOnTpnW7OH14ulR+wIBE2QuJ8I= 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.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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-20231011050154-1d073bb38998/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-20231019002500-864b42864d36 h1:bZQXbfLJ/7qq7CKZ7F1wgrY91SeBbuTcQtv6xjeHpMQ=
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20231019002500-864b42864d36/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= 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 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= 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= 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/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 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 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 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 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.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.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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/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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 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.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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/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.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 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 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 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= 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-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.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.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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
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/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= 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/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.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= 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.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= 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_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 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 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.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 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 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 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.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 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/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 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= 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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= 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/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= 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/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= 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-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 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -80,6 +80,28 @@ func (f *ParsedFlags) MustDeprecatedBool(deprecated string, newName string) bool
return f.MustBool(newName) 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. // MustInt returns the int value of a flag given by name.
// It panics if an error occurs. // It panics if an error occurs.
func (f *ParsedFlags) MustInt(name string) int { func (f *ParsedFlags) MustInt(name string) int {

View File

@@ -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) { func TestParsedFlags_MustInt(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError) fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.Int("foo", 0, "") fs.Int("foo", 0, "")

View File

@@ -6,53 +6,106 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// ModuleMock is a mock for the Module interface. // ModuleMock is a mock for the [Module] interface.
type ModuleMock struct { type ModuleMock struct {
DescriptorMock func() ModuleDescriptor DescriptorMock func() ModuleDescriptor
} }
func (mod ModuleMock) Descriptor() ModuleDescriptor { func (mod *ModuleMock) Descriptor() ModuleDescriptor {
return mod.DescriptorMock() return mod.DescriptorMock()
} }
// ValidatorMock is a mock for the Validator interface. // ValidatorMock is a mock for the [Validator] interface.
type ValidatorMock struct { type ValidatorMock struct {
ValidateMock func() error ValidateMock func() error
} }
func (mod ValidatorMock) Validate() error { func (mod *ValidatorMock) Validate() error {
return mod.ValidateMock() return mod.ValidateMock()
} }
// PDFEngineMock is a mock for the PDFEngine interface. // PDFEngineMock is a mock for the [PDFEngine] interface.
type PDFEngineMock struct { type PDFEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error 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 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) 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) 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 { type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error) PDFEngineMock func() (PDFEngine, error)
} }
func (provider PDFEngineProviderMock) PDFEngine() (PDFEngine, error) { func (provider *PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
return provider.PDFEngineMock() 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 { type LoggerProviderMock struct {
LoggerMock func(mod Module) (*zap.Logger, error) 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) return provider.LoggerMock(mod)
} }
@@ -62,5 +115,7 @@ var (
_ Validator = (*ValidatorMock)(nil) _ Validator = (*ValidatorMock)(nil)
_ PDFEngine = (*PDFEngineMock)(nil) _ PDFEngine = (*PDFEngineMock)(nil)
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil) _ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
_ Process = (*ProcessMock)(nil)
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
_ LoggerProvider = (*LoggerProviderMock)(nil) _ LoggerProvider = (*LoggerProviderMock)(nil)
) )

View File

@@ -8,7 +8,7 @@ import (
) )
func TestModuleMock(t *testing.T) { func TestModuleMock(t *testing.T) {
mock := ModuleMock{ mock := &ModuleMock{
DescriptorMock: func() ModuleDescriptor { DescriptorMock: func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return ModuleDescriptor{ID: "foo", New: func() Module {
return nil return nil
@@ -17,12 +17,12 @@ func TestModuleMock(t *testing.T) {
} }
if mock.Descriptor().ID != "foo" { 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) { func TestValidatorMock(t *testing.T) {
mock := ValidatorMock{ mock := &ValidatorMock{
ValidateMock: func() error { ValidateMock: func() error {
return nil return nil
}, },
@@ -30,12 +30,12 @@ func TestValidatorMock(t *testing.T) {
err := mock.Validate() err := mock.Validate()
if err != nil { 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) { func TestPDFEngineMock(t *testing.T) {
mock := PDFEngineMock{ mock := &PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -46,37 +46,119 @@ func TestPDFEngineMock(t *testing.T) {
err := mock.Merge(context.Background(), zap.NewNop(), nil, "") err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
if err != 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(), "", "", "") err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
if err != nil { 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) { func TestPDFEngineProviderMock(t *testing.T) {
mock := PDFEngineProviderMock{ mock := &PDFEngineProviderMock{
PDFEngineMock: func() (PDFEngine, error) { PDFEngineMock: func() (PDFEngine, error) {
return PDFEngineMock{}, nil return new(PDFEngineMock), nil
}, },
} }
_, err := mock.PDFEngine() _, err := mock.PDFEngine()
if err != nil { 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) { func TestLoggerProviderMock(t *testing.T) {
mock := LoggerProviderMock{ mock := &LoggerProviderMock{
LoggerMock: func(mod Module) (*zap.Logger, error) { LoggerMock: func(mod Module) (*zap.Logger, error) {
return nil, nil return nil, nil
}, },
} }
_, err := mock.Logger(ModuleMock{}) _, err := mock.Logger(new(ModuleMock))
if err != nil { 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
View 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)
)

View 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)
}
})
}
}

View File

@@ -20,6 +20,15 @@ func (ctx *ContextMock) SetDirPath(path string) {
ctx.dirPath = path 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. // SetValues sets the values.
// //
// ctx := &api.ContextMock{Context: &api.Context{}} // ctx := &api.ContextMock{Context: &api.Context{}}

View File

@@ -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) { func TestContextMock_SetValues(t *testing.T) {
mock := &ContextMock{&Context{}} mock := &ContextMock{&Context{}}
mock.SetValues(map[string][]string{ mock.SetValues(map[string][]string{

View 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)
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"os" "os"
"reflect" "reflect"
"regexp"
"testing" "testing"
"time" "time"
@@ -15,14 +14,6 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "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) { func TestDefaultOptions(t *testing.T) {
actual := DefaultOptions() actual := DefaultOptions()
notExpect := Options{} notExpect := Options{}
@@ -33,7 +24,7 @@ func TestDefaultOptions(t *testing.T) {
} }
func TestChromium_Descriptor(t *testing.T) { func TestChromium_Descriptor(t *testing.T) {
descriptor := Chromium{}.Descriptor() descriptor := new(Chromium).Descriptor()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Chromium)) expect := reflect.TypeOf(new(Chromium))
@@ -45,9 +36,9 @@ func TestChromium_Descriptor(t *testing.T) {
func TestChromium_Provision(t *testing.T) { func TestChromium_Provision(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
ctx *gotenberg.Context ctx *gotenberg.Context
expectErr bool expectError bool
}{ }{
{ {
scenario: "no logger provider", scenario: "no logger provider",
@@ -59,12 +50,12 @@ func TestChromium_Provision(t *testing.T) {
[]gotenberg.ModuleDescriptor{}, []gotenberg.ModuleDescriptor{},
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no logger from logger provider", scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct { mod := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -84,12 +75,12 @@ func TestChromium_Provision(t *testing.T) {
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no PDF engine provider", scenario: "no PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct { mod := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -109,12 +100,12 @@ func TestChromium_Provision(t *testing.T) {
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "no PDF engine from PDF engine provider", scenario: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct { mod := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock gotenberg.PDFEngineProviderMock
@@ -138,12 +129,12 @@ func TestChromium_Provision(t *testing.T) {
}, },
) )
}(), }(),
expectErr: true, expectError: true,
}, },
{ {
scenario: "provision success", scenario: "provision success",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct { mod := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock gotenberg.PDFEngineProviderMock
@@ -155,7 +146,7 @@ func TestChromium_Provision(t *testing.T) {
return zap.NewNop(), nil return zap.NewNop(), nil
} }
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) { mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil return new(gotenberg.PDFEngineMock), nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -169,123 +160,233 @@ func TestChromium_Provision(t *testing.T) {
}(), }(),
}, },
} { } {
mod := new(Chromium) t.Run(tc.scenario, func(t *testing.T) {
err := mod.Provision(tc.ctx) mod := new(Chromium)
err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil { if !tc.expectError && err != nil {
t.Errorf("test %s: expected error but got: %v", tc.scenario, err) t.Fatalf("expected no error but got: %v", err)
} }
if !tc.expectErr && err != nil { if tc.expectError && err == nil {
t.Errorf("test %s: expected no error but got: %v", tc.scenario, err) t.Fatal("expected error but got none")
} }
})
} }
} }
func TestChromium_Validate(t *testing.T) { func TestChromium_Validate(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
binPath string scenario string
expectErr bool binPath string
expectError bool
}{ }{
{ {
expectErr: true, scenario: "empty bin path",
binPath: "",
expectError: true,
}, },
{ {
binPath: "/foo", scenario: "bin path does not exist",
expectErr: true, 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) t.Run(tc.scenario, func(t *testing.T) {
mod.binPath = tc.binPath mod := new(Chromium)
err := mod.Validate() mod.args = browserArguments{
binPath: tc.binPath,
}
err := mod.Validate()
if tc.expectErr && err == nil { if !tc.expectError && err != nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Fatalf("expected no error but got: %v", err)
} }
if !tc.expectErr && err != nil { if tc.expectError && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) 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) { 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 { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
if len(metrics) != 2 { if len(metrics) != 4 {
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics)) t.Fatalf("expected %d metrics, but got %d", 4, len(metrics))
} }
actual := metrics[0].Read() actual := metrics[0].Read()
if actual != 0 { if actual != float64(1) {
t.Errorf("expected %d Chromium instances, but got %f", 0, actual) t.Errorf("expected %f for chromium_active_instances_count, but got %f", float64(1), actual)
} }
actual = metrics[1].Read() actual = metrics[1].Read()
if actual != 0 { if actual != float64(0) {
t.Errorf("expected %d Chromium failed starts, but got %f", 0, actual) 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) { func TestChromium_Checks(t *testing.T) {
tests := []struct { for _, tc := range []struct {
name string scenario string
mod Chromium supervisor gotenberg.ProcessSupervisor
tearUp func()
tearDown func()
expectAvailabilityStatus health.AvailabilityStatus expectAvailabilityStatus health.AvailabilityStatus
}{ }{
{ {
name: "ignore Chromium failed starts", scenario: "healthy module",
mod: Chromium{ supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
failedStartsThreshold: 0, return true
}, }},
},
{
name: "with Chromium failed starts threshold not reached",
mod: Chromium{
failedStartsThreshold: 1,
},
expectAvailabilityStatus: health.StatusUp, expectAvailabilityStatus: health.StatusUp,
}, },
{ {
name: "with Chromium failed starts threshold reached", scenario: "unhealthy module",
mod: Chromium{ supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
failedStartsThreshold: 1, return false
}, }},
tearUp: func() {
failedStartsCount = 1
},
tearDown: func() {
failedStartsCount = 0
},
expectAvailabilityStatus: health.StatusDown, expectAvailabilityStatus: health.StatusDown,
}, },
} } {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium)
mod.supervisor = tc.supervisor
for _, tc := range tests { checks, err := mod.Checks()
t.Run(tc.name, func(t *testing.T) {
if tc.tearUp != nil {
tc.tearUp()
}
checks, err := tc.mod.Checks()
if err != nil { if err != nil {
t.Fatalf("expected no error from mod.Checks(), but got: %v", err) t.Fatalf("expected no error 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))
} }
checker := health.NewChecker(checks...) checker := health.NewChecker(checks...)
@@ -294,10 +395,6 @@ func TestChromium_Checks(t *testing.T) {
if result.Status != tc.expectAvailabilityStatus { if result.Status != tc.expectAvailabilityStatus {
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status) 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) { func TestChromium_Routes(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
expectRoutes int expectRoutes int
disableRoutes bool disableRoutes bool
}{ }{
{ {
expectRoutes: 3, scenario: "routes not disabled",
expectRoutes: 3,
disableRoutes: false,
}, },
{ {
scenario: "routes disabled",
expectRoutes: 0,
disableRoutes: true, disableRoutes: true,
}, },
} { } {
mod := new(Chromium) t.Run(tc.scenario, func(t *testing.T) {
mod.disableRoutes = tc.disableRoutes mod := new(Chromium)
mod.disableRoutes = tc.disableRoutes
routes, err := mod.Routes() routes, err := mod.Routes()
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("expected no error but got: %v", err)
} }
if tc.expectRoutes != len(routes) { if tc.expectRoutes != len(routes) {
t.Errorf("test %d: expected %d routes but got %d", i, 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 { for _, tc := range []struct {
name string scenario string
timeout time.Duration supervisor gotenberg.ProcessSupervisor
cancel context.CancelFunc browser browser
URL string expectError bool
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
}{ }{
{ {
name: "context has no deadline", scenario: "PDF task success",
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html", browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
expectErr: true, return nil
}},
expectError: false,
}, },
{ {
name: "URL does not match the expression from the allowed list", scenario: "PDF task error",
timeout: time.Duration(60) * time.Second, browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", return errors.New("PDF task error")
allowList: regexp.MustCompile("file:///tmp/*"), }},
expectErr: true, expectError: 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",
}, },
} { } {
func() { t.Run(tc.scenario, func(t *testing.T) {
mod := new(Chromium) mod := new(Chromium)
mod.binPath = os.Getenv("CHROMIUM_BIN_PATH") mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
mod.userAgent = tc.userAgent return task()
mod.incognito = tc.incognito }}
mod.allowInsecureLocalhost = tc.allowInsecureLocalhost mod.browser = tc.browser
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
mod.disableWebSecurity = tc.disableWebSecurity
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
mod.hostResolverRules = tc.hostResolverRules
mod.proxyServer = tc.proxyServer
if tc.allowList == nil { err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
tc.allowList = regexp.MustCompile("")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
} }
if tc.denyList == nil { if tc.expectError && err == nil {
tc.denyList = regexp.MustCompile("") 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)
)

View File

@@ -7,21 +7,21 @@ import (
"go.uber.org/zap" "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. // Chromium.
type debugLogger struct { type debugLogger struct {
logger *zap.Logger logger *zap.Logger
} }
// Write logs the bytes in a debug message. // 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)) debug.logger.Debug(string(p))
return len(p), nil return len(p), nil
} }
// Printf logs a debug message. // 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...)) debug.logger.Debug(fmt.Sprintf(format, v...))
} }

View File

@@ -7,7 +7,7 @@ import (
) )
func TestDebugLogger_Write(t *testing.T) { 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")) expected := len([]byte("foo"))
if actual != expected { if actual != expected {
@@ -20,5 +20,5 @@ func TestDebugLogger_Write(t *testing.T) {
} }
func TestDebugLogger_Printf(t *testing.T) { func TestDebugLogger_Printf(t *testing.T) {
debugLogger{logger: zap.NewNop()}.Printf("%s", "foo") (&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo")
} }

View File

@@ -1,4 +1,4 @@
// Package chromium provides a module which adds routes for converting HTML // 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. // module.
package chromium package chromium

View 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)
)

View 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)
}
}

View File

@@ -21,9 +21,9 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "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. // 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() defaultOptions := DefaultOptions()
var ( var (
@@ -32,14 +32,14 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
waitWindowStatus string waitWindowStatus string
waitForExpression string waitForExpression string
userAgent string userAgent string
extraHTTPHeaders map[string]string extraHttpHeaders map[string]string
emulatedMediaType string emulatedMediaType string
landscape, printBackground, omitBackground bool landscape, printBackground, omitBackground bool
scale, paperWidth, paperHeight float64 scale, paperWidth, paperHeight float64
marginTop, marginBottom, marginLeft, marginRight float64 marginTop, marginBottom, marginLeft, marginRight float64
pageRanges string pageRanges string
headerTemplate, footerTemplate string headerTemplate, footerTemplate string
preferCSSPageSize bool preferCssPageSize bool
) )
form := ctx.FormData(). form := ctx.FormData().
@@ -47,15 +47,15 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("userAgent", &userAgent, defaultOptions.UserAgent). String("userAgent", &userAgent, ""). // FIXME: deprecated.
Custom("extraHttpHeaders", func(value string) error { Custom("extraHttpHeaders", func(value string) error {
if value == "" { if value == "" {
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders extraHttpHeaders = defaultOptions.ExtraHttpHeaders
return nil return nil
} }
err := json.Unmarshal([]byte(value), &extraHTTPHeaders) err := json.Unmarshal([]byte(value), &extraHttpHeaders)
if err != nil { if err != nil {
return fmt.Errorf("unmarshal extra HTTP headers: %w", err) 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). String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate). Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate). 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{ options := Options{
FailOnConsoleExceptions: failOnConsoleExceptions, FailOnConsoleExceptions: failOnConsoleExceptions,
WaitDelay: waitDelay, WaitDelay: waitDelay,
WaitWindowStatus: waitWindowStatus, WaitWindowStatus: waitWindowStatus,
WaitForExpression: waitForExpression, WaitForExpression: waitForExpression,
UserAgent: userAgent, ExtraHttpHeaders: extraHttpHeaders,
ExtraHTTPHeaders: extraHTTPHeaders,
ExtraLinkTags: defaultOptions.ExtraLinkTags,
EmulatedMediaType: emulatedMediaType, EmulatedMediaType: emulatedMediaType,
ExtraScriptTags: defaultOptions.ExtraScriptTags,
Landscape: landscape, Landscape: landscape,
PrintBackground: printBackground, PrintBackground: printBackground,
OmitBackground: omitBackground, OmitBackground: omitBackground,
@@ -115,60 +123,36 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
PageRanges: pageRanges, PageRanges: pageRanges,
HeaderTemplate: headerTemplate, HeaderTemplate: headerTemplate,
FooterTemplate: footerTemplate, FooterTemplate: footerTemplate,
PreferCSSPageSize: preferCSSPageSize, PreferCssPageSize: preferCssPageSize,
} }
return form, options return form, options
} }
// convertURLRoute returns an api.Route which can convert a URL to PDF. // convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route { func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/url", Path: "/forms/chromium/convert/url",
IsMultipart: true, IsMultipart: true,
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
var ( var (
URL string url string
PDFformat string pdfFormat string
) )
err := form. err := form.
MandatoryString("url", &URL). MandatoryString("url", &url).
String("pdfFormat", &PDFformat, ""). 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
}).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) 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 { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) 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. // convertHtmlRoute returns an [api.Route] which can convert an HTML file to
func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route { // PDF.
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/html", Path: "/forms/chromium/convert/html",
IsMultipart: true, IsMultipart: true,
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
var ( var (
inputPath string inputPath string
PDFformat string pdfFormat string
) )
err := form. err := form.
MandatoryPath("index.html", &inputPath). MandatoryPath("index.html", &inputPath).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &pdfFormat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) 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 { if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err) 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. // to PDF.
func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route { func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/markdown", Path: "/forms/chromium/convert/markdown",
IsMultipart: true, IsMultipart: true,
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPDFOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
var ( var (
inputPath string inputPath string
markdownPaths []string markdownPaths []string
PDFformat string pdfFormat string
) )
err := form. err := form.
MandatoryPath("index.html", &inputPath). MandatoryPath("index.html", &inputPath).
MandatoryPaths([]string{".md"}, &markdownPaths). MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &pdfFormat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err) 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. // 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 { func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url, pdfFormat string, options Options) error {
outputPath := ctx.GeneratePath(".pdf") 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 err != nil {
if errors.Is(err, ErrURLNotAuthorized) { if errors.Is(err, ErrUrlNotAuthorized) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHTTPError(
http.StatusForbidden, 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 // Now, let's check if the client want to convert this result PDF
// to a specific PDF format. // to a specific PDF format.
if PDFformat != "" { if pdfFormat != "" {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") 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 err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) { 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), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHTTPError(
http.StatusBadRequest, 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

View 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
}
}
}
}

View File

@@ -29,7 +29,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider1 := struct { provider1 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}
@@ -42,7 +42,7 @@ func TestLibreOffice_Provision(t *testing.T) {
return uno.APIMock{}, nil return uno.APIMock{}, nil
} }
provider2 := struct { provider2 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineProviderMock gotenberg.PDFEngineProviderMock
}{} }{}
@@ -52,7 +52,7 @@ func TestLibreOffice_Provision(t *testing.T) {
}} }}
} }
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) { provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil return &gotenberg.PDFEngineMock{}, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -79,7 +79,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{ {
name: "no API from UNO API provider", name: "no API from UNO API provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}
@@ -106,7 +106,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{ {
name: "no PDF engine provider", name: "no PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}
@@ -133,7 +133,7 @@ func TestLibreOffice_Provision(t *testing.T) {
{ {
name: "no PDF engine from PDF engine provider", name: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider1 := struct { provider1 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}
@@ -146,7 +146,7 @@ func TestLibreOffice_Provision(t *testing.T) {
return uno.APIMock{}, nil return uno.APIMock{}, nil
} }
provider2 := struct { provider2 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineProviderMock gotenberg.PDFEngineProviderMock
}{} }{}
@@ -156,7 +156,7 @@ func TestLibreOffice_Provision(t *testing.T) {
}} }}
} }
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) { provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, errors.New("foo") return &gotenberg.PDFEngineMock{}, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(

View File

@@ -32,7 +32,7 @@ func TestUNO_Provider(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}
@@ -68,7 +68,7 @@ func TestUNO_Provider(t *testing.T) {
{ {
name: "no API from UNO API provider", name: "no API from UNO API provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
uno.ProviderMock uno.ProviderMock
}{} }{}

View File

@@ -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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil 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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo") 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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil 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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil 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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil 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 { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil 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 { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil 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 { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil 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 { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo") 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 { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable return gotenberg.ErrPDFFormatNotAvailable
}, },

View File

@@ -35,7 +35,7 @@ func TestUNO_Provision(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -61,7 +61,7 @@ func TestUNO_Provision(t *testing.T) {
{ {
name: "threshold from deprecated flag --unoconv-disable-listener", name: "threshold from deprecated flag --unoconv-disable-listener",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -107,7 +107,7 @@ func TestUNO_Provision(t *testing.T) {
{ {
name: "no logger from logger provider", name: "no logger from logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}

View File

@@ -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. // modules.
package logging package logging

View File

@@ -15,7 +15,7 @@ import (
) )
func init() { func init() {
gotenberg.MustRegisterModule(Logging{}) gotenberg.MustRegisterModule(new(Logging))
} }
const ( const (
@@ -31,15 +31,16 @@ const (
textLoggingFormat = "text" 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 { type Logging struct {
level string level string
format string format string
fieldsPrefix string fieldsPrefix string
} }
// Descriptor returns a Logging's module descriptor. // Descriptor returns a [Logging]'s module descriptor.
func (Logging) Descriptor() gotenberg.ModuleDescriptor { func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ return gotenberg.ModuleDescriptor{
ID: "logging", ID: "logging",
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
@@ -66,7 +67,7 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
} }
// Validate validates the log level and format. // Validate validates the log level and format.
func (log Logging) Validate() error { func (log *Logging) Validate() error {
var err error var err error
switch log.level { switch log.level {
@@ -92,8 +93,8 @@ func (log Logging) Validate() error {
return err return err
} }
// Logger returns a zap.Logger. // Logger returns a [zap.Logger].
func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) { func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
if logger == nil { if logger == nil {
lvl, err := newLogLevel(log.level) lvl, err := newLogLevel(log.level)
if err != nil { if err != nil {

View File

@@ -13,7 +13,7 @@ import (
) )
func TestLogging_Descriptor(t *testing.T) { func TestLogging_Descriptor(t *testing.T) {
descriptor := Logging{}.Descriptor() descriptor := new(Logging).Descriptor()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Logging)) expect := reflect.TypeOf(new(Logging))
@@ -56,66 +56,68 @@ func TestLogging_Provision(t *testing.T) {
expectFieldsPrefix: "", expectFieldsPrefix: "",
}, },
} { } {
var flags []string t.Run(tc.scenario, func(t *testing.T) {
var flags []string
if tc.level != "" { if tc.level != "" {
flags = append(flags, "--log-level", tc.level) flags = append(flags, "--log-level", tc.level)
} }
if tc.format != "" { if tc.format != "" {
flags = append(flags, "--log-format", tc.format) flags = append(flags, "--log-format", tc.format)
} }
if tc.fieldsPrefix != "" { if tc.fieldsPrefix != "" {
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix) flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
} }
logging := new(Logging) logging := new(Logging)
fs := logging.Descriptor().FlagSet fs := logging.Descriptor().FlagSet
err := fs.Parse(flags) err := fs.Parse(flags)
if err != nil { if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err) 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) err = logging.Provision(ctx)
if err != nil { if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err) t.Fatalf("expected no error while provisioning but got: %v", err)
} }
if logging.level != tc.expectLevel { if logging.level != tc.expectLevel {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectLevel, logging.level) t.Errorf("expected logging level '%s' but got '%s'", tc.expectLevel, logging.level)
} }
if logging.format != tc.expectFormat { if logging.format != tc.expectFormat {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFormat, logging.format) t.Errorf("expected logging format '%s' but got '%s'", tc.expectFormat, logging.format)
} }
if logging.fieldsPrefix != tc.expectFieldsPrefix { if logging.fieldsPrefix != tc.expectFieldsPrefix {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFieldsPrefix, logging.fieldsPrefix) t.Errorf("expected logging fields prefix '%s' but got '%s'", tc.expectFieldsPrefix, logging.fieldsPrefix)
} }
})
} }
} }
func TestLogging_Validate(t *testing.T) { func TestLogging_Validate(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
level string level string
format string format string
expectErr bool expectError bool
}{ }{
{ {
scenario: "invalid level", scenario: "invalid level",
level: "foo", level: "foo",
expectErr: true, expectError: true,
}, },
{ {
scenario: "invalid format", scenario: "invalid format",
level: debugLoggingLevel, level: debugLoggingLevel,
format: "foo", format: "foo",
expectErr: true, expectError: true,
}, },
{ {
scenario: "valid level and format", scenario: "valid level and format",
@@ -129,11 +131,11 @@ func TestLogging_Validate(t *testing.T) {
err := logging.Validate() err := logging.Validate()
if tc.expectErr && err == nil { if tc.expectError && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err) 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) t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
} }
} }
@@ -145,18 +147,18 @@ func TestLogging_Logger(t *testing.T) {
level string level string
format string format string
fieldsPrefix string fieldsPrefix string
expectErr bool expectError bool
}{ }{
{ {
scenario: "invalid level", scenario: "invalid level",
level: "foo", level: "foo",
expectErr: true, expectError: true,
}, },
{ {
scenario: "invalid format", scenario: "invalid format",
level: debugLoggingLevel, level: debugLoggingLevel,
format: "foo", format: "foo",
expectErr: true, expectError: true,
}, },
{ {
scenario: "valid level and format", scenario: "valid level and format",
@@ -164,24 +166,26 @@ func TestLogging_Logger(t *testing.T) {
format: autoLoggingFormat, format: autoLoggingFormat,
}, },
} { } {
logging := new(Logging) t.Run(tc.scenario, func(t *testing.T) {
logging.level = tc.level logging := new(Logging)
logging.format = tc.format logging.level = tc.level
logging.fieldsPrefix = tc.fieldsPrefix logging.format = tc.format
logging.fieldsPrefix = tc.fieldsPrefix
_, err := logging.Logger(gotenberg.ModuleMock{ _, err := logging.Logger(&gotenberg.ModuleMock{
DescriptorMock: func() gotenberg.ModuleDescriptor { DescriptorMock: func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "mock", New: nil} 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, level: zapcore.ErrorLevel,
}, },
} { } {
core, obsvr := observer.New(tc.level) t.Run(tc.scenario, func(t *testing.T) {
lgr := zap.New(customCore{ core, obsvr := observer.New(tc.level)
Core: core, lgr := zap.New(customCore{
fieldsPrefix: tc.fieldsPrefix, Core: core,
}).With(zap.String("a_field", "a value")) 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 { if tc.expectEntry && len(entries) == 0 {
t.Fatalf("%s: expected an entry", tc.scenario) t.Fatal("expected an entry")
}
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 fields[0].Key != fmt.Sprintf("%sa_field", prefix) { if !tc.expectEntry && len(entries) != 0 {
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key) t.Fatal("expected no entry")
} }
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) { var prefix string
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key) 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 scenario string
level string level string
expectZapLevel zapcore.Level expectZapLevel zapcore.Level
expectErr bool expectError bool
}{ }{
{ {
scenario: "error level", scenario: "error level",
@@ -280,30 +286,32 @@ func Test_newLogLevel(t *testing.T) {
scenario: "invalid level", scenario: "invalid level",
level: "foo", level: "foo",
expectZapLevel: zapcore.InvalidLevel, 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 { if tc.expectError && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err) t.Fatal("expected error but got none")
} }
if !tc.expectErr && err != nil { if !tc.expectError && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err) t.Fatalf("expected no error but got: %v", err)
} }
if tc.expectZapLevel != actual { if tc.expectZapLevel != actual {
t.Errorf("%s: expected %d level but got %d", tc.scenario, tc.expectZapLevel, actual) t.Errorf("expected %d level but got %d", tc.expectZapLevel, actual)
} }
})
} }
} }
func Test_newLogEncoder(t *testing.T) { func Test_newLogEncoder(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
format string format string
expectErr bool expectError bool
}{ }{
{ {
scenario: "auto format", scenario: "auto format",
@@ -318,19 +326,21 @@ func Test_newLogEncoder(t *testing.T) {
format: jsonLoggingFormat, format: jsonLoggingFormat,
}, },
{ {
scenario: "invalid format", scenario: "invalid format",
format: "foo", format: "foo",
expectErr: true, expectError: true,
}, },
} { } {
_, err := newLogEncoder(tc.format) t.Run(tc.scenario, func(t *testing.T) {
_, err := newLogEncoder(tc.format)
if tc.expectErr && err == nil { if tc.expectError && err == nil {
t.Errorf("%s: expected error but got: %v", tc.scenario, err) t.Fatal("expected error but got none")
} }
if !tc.expectErr && err != nil { if !tc.expectError && err != nil {
t.Errorf("%s: expected no error but got: %v", tc.scenario, err) t.Errorf("expected no error but got: %v", err)
} }
})
} }
} }

View File

@@ -40,7 +40,7 @@ func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
return nil 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 { func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf) err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
if err == nil { 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) 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 { 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. // Interface guards.

View File

@@ -20,7 +20,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -31,12 +31,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{ {
name: "at least one engine does not return an error", name: "at least one engine does not return an error",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -47,12 +47,12 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{ {
name: "all engines return an error", name: "all engines return an error",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
@@ -64,7 +64,7 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
{ {
name: "context expired", name: "context expired",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -105,7 +105,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },
@@ -116,12 +116,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{ {
name: "at least one engine does not return an error", name: "at least one engine does not return an error",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },
@@ -132,12 +132,12 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{ {
name: "all engines return an error", name: "all engines return an error",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
@@ -149,7 +149,7 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
{ {
name: "context expired", name: "context expired",
engine: newMultiPDFEngines( engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{ &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },

View File

@@ -32,7 +32,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{ {
name: "no selection from user", name: "no selection from user",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -45,7 +45,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil return zap.NewNop(), nil
} }
engine := struct { engine := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.ValidatorMock gotenberg.ValidatorMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
@@ -72,7 +72,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{ {
name: "selection from user", name: "selection from user",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -85,7 +85,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil return zap.NewNop(), nil
} }
engine1 := struct { engine1 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.ValidatorMock gotenberg.ValidatorMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
@@ -97,7 +97,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return nil return nil
} }
engine2 := struct { engine2 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.ValidatorMock gotenberg.ValidatorMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
@@ -131,7 +131,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{ {
name: "user select deprecated unoconv-pdfengine", name: "user select deprecated unoconv-pdfengine",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -144,7 +144,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil return zap.NewNop(), nil
} }
engine := struct { engine := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.ValidatorMock gotenberg.ValidatorMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
@@ -189,7 +189,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{ {
name: "no logger from logger provider", name: "no logger from logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -216,7 +216,7 @@ func TestPDFEngines_Provision(t *testing.T) {
{ {
name: "no valid PDF engines", name: "no valid PDF engines",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
provider := struct { provider := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.LoggerProviderMock gotenberg.LoggerProviderMock
}{} }{}
@@ -229,7 +229,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return zap.NewNop(), nil return zap.NewNop(), nil
} }
engine := struct { engine := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.ValidatorMock gotenberg.ValidatorMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
@@ -292,7 +292,7 @@ func TestPDFEngines_Validate(t *testing.T) {
name: "existing PDF engine", name: "existing PDF engine",
names: []string{"foo"}, names: []string{"foo"},
engines: func() []gotenberg.PDFEngine { engines: func() []gotenberg.PDFEngine {
engine := struct { engine := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
}{} }{}
@@ -309,7 +309,7 @@ func TestPDFEngines_Validate(t *testing.T) {
name: "non-existing bar PDF engine", name: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"}, names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PDFEngine { engines: func() []gotenberg.PDFEngine {
engine1 := struct { engine1 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
}{} }{}
@@ -317,7 +317,7 @@ func TestPDFEngines_Validate(t *testing.T) {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
} }
engine2 := struct { engine2 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
}{} }{}
@@ -377,7 +377,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
mod := PDFEngines{ mod := PDFEngines{
names: []string{"foo", "bar"}, names: []string{"foo", "bar"},
engines: func() []gotenberg.PDFEngine { engines: func() []gotenberg.PDFEngine {
engine1 := struct { engine1 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
}{} }{}
@@ -385,7 +385,7 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
} }
engine2 := struct { engine2 := &struct {
gotenberg.ModuleMock gotenberg.ModuleMock
gotenberg.PDFEngineMock gotenberg.PDFEngineMock
}{} }{}
@@ -416,7 +416,7 @@ func TestPDFEngines_Routes(t *testing.T) {
name: "route not disabled", name: "route not disabled",
mod: PDFEngines{ mod: PDFEngines{
engines: []gotenberg.PDFEngine{ engines: []gotenberg.PDFEngine{
gotenberg.PDFEngineMock{}, &gotenberg.PDFEngineMock{},
}, },
}, },
expectRoutesCount: 2, expectRoutesCount: 2,

View File

@@ -33,7 +33,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -57,7 +57,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
@@ -79,7 +79,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -104,7 +104,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -129,7 +129,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -152,7 +152,7 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil return nil
}, },
@@ -226,7 +226,7 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },
@@ -250,7 +250,7 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },
@@ -302,7 +302,7 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
@@ -324,7 +324,7 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable return gotenberg.ErrPDFFormatNotAvailable
}, },
@@ -349,7 +349,7 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{ engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil return nil
}, },

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

91
test/testdata/chromium/html/index.html vendored Normal file
View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
body {
font-family: Arial, Helvetica, sans-serif;
}
.center {
text-align: center;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -25,4 +25,5 @@ body {
.page-break-after { .page-break-after {
page-break-after: always; page-break-after: always;
} }
} }

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -20,15 +20,24 @@
</div> </div>
<div class="page-break-after"> <div class="page-break-after">
{{ toHTML "markdown1.md" }} {{ toHTML "defaultfont.md" }}
<div class="google-font"> <div class="google-font">
{{ toHTML "markdown2.md" }} {{ toHTML "googlefont.md" }}
</div> </div>
<div class="local-font"> <div class="local-font">
{{ toHTML "markdown3.md" }} {{ toHTML "localfont.md" }}
</div> </div>
</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> </body>
</html> </html>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,3 +1,5 @@
## This paragraph displays a table from a markdown file
| Tables | Are | Cool | | Tables | Are | Cool |
|----------|:-------------:|------:| |----------|:-------------:|------:|
| col 1 is | left-aligned | $1600 | | col 1 is | left-aligned | $1600 |

View File

@@ -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>

View File

@@ -1,13 +0,0 @@
<html>
<head>
<style>
body {
font-size: 8rem;
margin: 4rem auto;
}
</style>
</head>
<body>
<span class="title"></span>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff