feat: add more PDF formats, drastically improve LibreOffice long-running instance management

This commit is contained in:
Julien Neuhart
2022-02-07 18:28:58 +01:00
parent 3d855712ee
commit 24817d5707
34 changed files with 3883 additions and 2377 deletions

View File

@@ -73,22 +73,37 @@ func (cmd Cmd) Start() error {
return nil
}
// Wait waits for the command to complete. It should be called when using the
// Start method, so that the command does not leak zombies.
func (cmd Cmd) Wait() error {
err := cmd.process.Wait()
if err != nil {
return fmt.Errorf("wait for unix process: %w", err)
}
return nil
}
// Exec executes the command and wait for its completion or until the context
// is done. In any case, it kills the unix process and all its children.
func (cmd Cmd) Exec() error {
func (cmd Cmd) Exec() (int, error) {
if cmd.ctx == nil {
return errors.New("nil context")
return 10, errors.New("nil context")
}
err := cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("start command: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
}
errChan := make(chan error, 1)
go func() {
errChan <- cmd.process.Wait()
errChan <- cmd.Wait()
}()
select {
@@ -99,17 +114,21 @@ func (cmd Cmd) Exec() error {
}
if err == nil {
return nil
return 0, nil
}
return fmt.Errorf("unix process error: %w", err)
if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("unix process error: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
case <-cmd.ctx.Done():
errProc := cmd.Kill()
if errProc != nil {
cmd.logger.Error(errProc.Error())
}
return fmt.Errorf("context done: %w", cmd.ctx.Err())
return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
}
}

View File

@@ -12,209 +12,313 @@ func TestCommand(t *testing.T) {
cmd := Command(zap.NewNop(), "foo")
if !cmd.process.SysProcAttr.Setpgid {
t.Error("expected Setpgid to be true")
t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
}
}
func TestCommandContext(t *testing.T) {
for i, tc := range []struct {
ctx context.Context
expectErr bool
tests := []struct {
name string
ctx context.Context
expectCommandContextErr bool
}{
{
ctx: nil,
expectErr: true,
name: "nominal behavior",
ctx: context.Background(),
},
{
ctx: context.TODO(),
name: "nil context",
expectCommandContextErr: true,
},
} {
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
}
if err == nil && !cmd.process.SysProcAttr.Setpgid {
t.Fatalf("test %d: expected Setpgid to be true", i)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if err == nil && !cmd.process.SysProcAttr.Setpgid {
t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectCommandContextErr && err == nil {
t.Error("expected error from CommandContext(), but got none")
}
if !tc.expectCommandContextErr && err != nil {
t.Errorf("expected no error from CommandContext(), but got: %v", err)
}
})
}
}
func TestCmd_Start(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
expectErr bool
tests := []struct {
name string
cmd Cmd
expectStartErr bool
}{
{
cmd: Command(zap.NewNop(), "foo"),
expectErr: true,
name: "nominal behavior",
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
},
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
name: "start error",
cmd: Command(zap.NewNop(), "foo"),
expectStartErr: true,
},
} {
err := tc.cmd.Start()
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.Start()
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectStartErr && err == nil {
t.Error("expected error from cmd.Start(), but got none")
}
if !tc.expectStartErr && err != nil {
t.Errorf("expected no error from cmd.Start(), but got: %v", err)
}
})
}
}
func TestCmd_Wait(t *testing.T) {
tests := []struct {
name string
cmd Cmd
expectWaitErr bool
}{
{
name: "nominal behavior",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.Start()
if err != nil {
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
}
return cmd
}(),
},
{
name: "wait error",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.Start()
if err != nil {
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
}
err = cmd.Kill()
if err != nil {
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
}
return cmd
}(),
expectWaitErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.Wait()
if tc.expectWaitErr && err == nil {
t.Error("expected error from cmd.Wait(), but got none")
}
if !tc.expectWaitErr && err != nil {
t.Errorf("expected no error from cmd.Wait(), but got: %v", err)
}
})
}
}
func TestCmd_Exec(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
timeout time.Duration
expectErr bool
tests := []struct {
name string
cmd Cmd
timeout time.Duration
expectExecErr bool
}{
{
cmd: Command(zap.NewNop(), "foo"),
expectErr: true,
name: "nominal behavior",
cmd: func() Cmd {
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
if err != nil {
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
}
return cmd
}(),
},
{
cmd: Command(zap.NewNop(), "foo"),
timeout: time.Duration(5) * time.Second,
expectErr: true,
name: "nil context",
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
expectExecErr: true,
},
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
timeout: time.Duration(5) * time.Second,
name: "start error",
cmd: func() Cmd {
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
if err != nil {
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
}
return cmd
}(),
expectExecErr: true,
},
{
cmd: Command(zap.NewNop(), "sleep", "3"),
timeout: time.Duration(2) * time.Second,
expectErr: true,
name: "context done",
cmd: Command(zap.NewNop(), "sleep", "2"),
timeout: time.Duration(1) * time.Second,
expectExecErr: true,
},
} {
if tc.timeout > 0 {
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
defer cancel()
}
tc.cmd.ctx = ctx
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.timeout > 0 {
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
defer cancel()
err := tc.cmd.Exec()
tc.cmd.ctx = ctx
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
_, err := tc.cmd.Exec()
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectExecErr && err == nil {
t.Error("expected error from cmd.Exec(), but got none")
}
if !tc.expectExecErr && err != nil {
t.Errorf("expected no error from cmd.Exec(), but got: %v", err)
}
})
}
}
func TestCmd_pipeOutput(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
run bool
expectErr bool
tests := []struct {
name string
cmd Cmd
run bool
expectPipeOutputErr bool
}{
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
name: "nominal behavior",
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
run: true,
},
{
name: "no debug, no pipe",
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
},
{
name: "stdout already piped",
cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StdoutPipe()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
t.Fatalf("expected no error from cmd.process.StdoutPipe(), but got: %v", err)
}
return cmd
}(),
expectErr: true,
expectPipeOutputErr: true,
},
{
name: "stderr already piped",
cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StderrPipe()
_, err := cmd.process.StderrPipe()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
t.Fatalf("expected no error from cmd.process.StderrPipe(), but got: %v", err)
}
return cmd
}(),
expectErr: true,
expectPipeOutputErr: true,
},
{
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
run: true,
},
} {
err := tc.cmd.pipeOutput()
}
if tc.run {
errStart := tc.cmd.process.Start()
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.pipeOutput()
if errStart != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
if tc.run {
errStart := tc.cmd.process.Start()
if errStart != nil {
t.Fatalf("expected no error from tc.cmd.process.Start(), but got: %v", errStart)
}
}
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if tc.expectPipeOutputErr && err == nil {
t.Error("expected error from cmd.pipeOutput(), but got none")
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if !tc.expectPipeOutputErr && err != nil {
t.Errorf("expected no error from cmd.pipeOutput(), but got: %v", err)
}
})
}
}
func TestCmd_Kill(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
expectErr bool
tests := []struct {
name string
cmd Cmd
}{
{
cmd: Cmd{logger: zap.NewNop()},
},
{
name: "nominal behavior",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "sleep", "60")
err := cmd.process.Start()
err := cmd.process.Start()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
}
return cmd
}(),
},
{
name: "no process",
cmd: Cmd{logger: zap.NewNop()},
},
{
name: "process already killed",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.process.Run()
cmd := Command(zap.NewNop(), "sleep", "60")
err := cmd.process.Start()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
}
err = cmd.Kill()
if err != nil {
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
}
return cmd
}(),
},
} {
err := tc.cmd.Kill()
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.Kill()
if err != nil {
t.Errorf("expected no error from cmd.Kill(), but got: %v", err)
}
})
}
}

66
pkg/gotenberg/mocks.go Normal file
View File

@@ -0,0 +1,66 @@
package gotenberg
import (
"context"
"go.uber.org/zap"
)
// ModuleMock is a mock for the Module interface.
type ModuleMock struct {
DescriptorMock func() ModuleDescriptor
}
func (mod ModuleMock) Descriptor() ModuleDescriptor {
return mod.DescriptorMock()
}
// ValidatorMock is a mock for the Validator interface.
type ValidatorMock struct {
ValidateMock func() error
}
func (mod ValidatorMock) Validate() error {
return mod.ValidateMock()
}
// PDFEngineMock is a mock for the PDFEngine interface.
type PDFEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
}
func (engine PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
}
func (engine PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
}
// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error)
}
func (provider PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
return provider.PDFEngineMock()
}
// LoggerProviderMock is a mock for the LoggerProvider interface.
type LoggerProviderMock struct {
LoggerMock func(mod Module) (*zap.Logger, error)
}
func (provider LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
return provider.LoggerMock(mod)
}
// Interface guards.
var (
_ Module = (*ModuleMock)(nil)
_ Validator = (*ValidatorMock)(nil)
_ PDFEngine = (*PDFEngineMock)(nil)
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
_ LoggerProvider = (*LoggerProviderMock)(nil)
)

View File

@@ -0,0 +1,82 @@
package gotenberg
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestModuleMock(t *testing.T) {
mock := ModuleMock{
DescriptorMock: func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module {
return nil
}}
},
}
if mock.Descriptor().ID != "foo" {
t.Errorf("expected ID '%s' from mock.Descriptor(), but got '%s'", "foo", mock.Descriptor().ID)
}
}
func TestValidatorMock(t *testing.T) {
mock := ValidatorMock{
ValidateMock: func() error {
return nil
},
}
err := mock.Validate()
if err != nil {
t.Errorf("expected no error from mock.Validate(), but got: %v", err)
}
}
func TestPDFEngineMock(t *testing.T) {
mock := PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
}
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
if err != nil {
t.Errorf("expected no error from mock.Merge(), but got: %v", err)
}
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
if err != nil {
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
}
}
func TestPDFEngineProvider(t *testing.T) {
mock := PDFEngineProviderMock{
PDFEngineMock: func() (PDFEngine, error) {
return PDFEngineMock{}, nil
},
}
_, err := mock.PDFEngine()
if err != nil {
t.Errorf("expected no error from mock.PDFEngine(), but got: %v", err)
}
}
func TestLoggerProviderMock(t *testing.T) {
mock := LoggerProviderMock{
LoggerMock: func(mod Module) (*zap.Logger, error) {
return nil, nil
},
}
_, err := mock.Logger(ModuleMock{})
if err != nil {
t.Errorf("expected no error from mock.Logger(), but got: %v", err)
}
}