1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-10-20 19:52:06 +00:00
forgejo-runner/act/jobparser/model_test.go
Mathieu Fenniak 3f52c56d1e feat: improve readability of error messages from ParseRawOn (#1063)
With https://codeberg.org/forgejo/forgejo/pulls/9530, the error messages from `ParseRawOn` are user-facing and need a pass to improve their meaning.

<!--start release-notes-assistant-->
<!--URL:https://code.forgejo.org/forgejo/runner-->
- features
  - [PR](https://code.forgejo.org/forgejo/runner/pulls/1063): <!--number 1063 --><!--line 0 --><!--description ZmVhdDogaW1wcm92ZSByZWFkYWJpbGl0eSBvZiBlcnJvciBtZXNzYWdlcyBmcm9tIFBhcnNlUmF3T24=-->feat: improve readability of error messages from ParseRawOn<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1063
Reviewed-by: earl-warren <earl-warren@noreply.code.forgejo.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2025-10-07 14:41:55 +00:00

527 lines
11 KiB
Go

package jobparser
import (
"fmt"
"strings"
"testing"
"code.forgejo.org/forgejo/runner/v11/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
)
func TestParseRawOn(t *testing.T) {
kases := []struct {
input string
result []*Event
err string
}{
{
input: "on: issue_comment",
result: []*Event{
{
Name: "issue_comment",
},
},
},
{
input: "on:\n push",
result: []*Event{
{
Name: "push",
},
},
},
{
input: "on: [123]",
err: "value at index 0 was unexpected type int; must be a string but was 123",
},
{
input: "on:\n - push\n - pull_request",
result: []*Event{
{
Name: "push",
},
{
Name: "pull_request",
},
},
},
{
input: "on: { push: null }",
result: []*Event{
{
Name: "push",
acts: map[string][]string{},
},
},
},
{
input: "on: { push: 'abc' }",
err: "key \"push\" had unexpected type string; expected a map or array but was \"abc\"",
},
{
input: "on:\n push:\n branches:\n - master",
result: []*Event{
{
Name: "push",
acts: map[string][]string{
"branches": {
"master",
},
},
},
},
},
{
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
result: []*Event{
{
Name: "branch_protection_rule",
acts: map[string][]string{
"types": {
"created",
"deleted",
},
},
},
},
},
{
input: "on:\n branch_protection_rule:\n types: [123, deleted]",
err: "key \"branch_protection_rule\".\"types\" index 0 had unexpected type int; a string was expected but was 123",
},
{
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
result: []*Event{
{
Name: "project",
acts: map[string][]string{
"types": {
"created",
"deleted",
},
},
},
{
Name: "milestone",
acts: map[string][]string{
"types": {
"opened",
"deleted",
},
},
},
},
},
{
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
result: []*Event{
{
Name: "pull_request",
acts: map[string][]string{
"types": {
"opened",
},
"branches": {
"releases/**",
},
},
},
},
},
{
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
result: []*Event{
{
Name: "push",
acts: map[string][]string{
"branches": {
"main",
},
},
},
{
Name: "pull_request",
acts: map[string][]string{
"types": {
"opened",
},
"branches": {
"**",
},
},
},
},
},
{
input: "on:\n push:\n branches:\n - 'main'\n - 'releases/**'",
result: []*Event{
{
Name: "push",
acts: map[string][]string{
"branches": {
"main",
"releases/**",
},
},
},
},
},
{
input: "on:\n push:\n tags:\n - v1.**",
result: []*Event{
{
Name: "push",
acts: map[string][]string{
"tags": {
"v1.**",
},
},
},
},
},
{
input: "on: [pull_request, workflow_dispatch, workflow_call]",
result: []*Event{
{
Name: "pull_request",
},
{
Name: "workflow_dispatch",
},
{
Name: "workflow_call",
},
},
},
{
input: "on:\n schedule:\n - cron: '20 6 * * *'",
result: []*Event{
{
Name: "schedule",
schedules: []map[string]string{
{
"cron": "20 6 * * *",
},
},
},
},
},
{
input: "on:\n schedule2:\n - cron: '20 6 * * *'",
err: "key \"schedule2\" had an type []interface {}; only the 'schedule' key is expected with this type",
},
{
input: "on:\n schedule:\n - 123",
err: "key \"schedule\"[0] had unexpected type int; a map with a key \"cron\" was expected, but value was 123",
},
{
input: "on:\n schedule:\n - corn: '20 6 * * *'",
err: "key \"schedule\"[0] had unexpected key \"corn\"; \"cron\" was expected",
},
{
input: "on:\n schedule:\n - cron: 123",
err: "key \"schedule\"[0].\"cron\" had unexpected type int; a string was expected by was 123",
},
{
input: `
on:
workflow_dispatch:
inputs:
test:
type: string
silently: ignore
`,
result: []*Event{
{
Name: "workflow_dispatch",
},
},
},
{
input: `
on:
workflow_call:
inputs:
test:
type: string
outputs:
output:
value: something
silently: ignore
`,
result: []*Event{
{
Name: "workflow_call",
},
},
},
{
input: `
on:
workflow_call:
mistake:
access-token:
description: 'A token passed from the caller workflow'
required: false
`,
err: "invalid value on key \"workflow_call\": workflow_call only supports keys \"inputs\" and \"outputs\", but key \"mistake\" was found",
},
{
input: `
on:
workflow_call: { map: 123 }
`,
err: "key \"workflow_call\".\"map\" had unexpected type int; was 123",
},
}
for _, kase := range kases {
t.Run(kase.input, func(t *testing.T) {
origin, err := model.ReadWorkflow(strings.NewReader(kase.input), false)
require.NoError(t, err)
events, err := ParseRawOn(&origin.RawOn)
if kase.err != "" {
assert.ErrorContains(t, err, kase.err)
} else {
assert.NoError(t, err)
assert.EqualValues(t, kase.result, events, fmt.Sprintf("%#v", events))
}
})
}
}
func TestSingleWorkflow_SetJob(t *testing.T) {
t.Run("erase needs", func(t *testing.T) {
content := ReadTestdata(t, "erase_needs.in.yaml")
want := ReadTestdata(t, "erase_needs.out.yaml")
swf, err := Parse(content, false)
require.NoError(t, err)
builder := &strings.Builder{}
for _, v := range swf {
id, job := v.Job()
require.NoError(t, v.SetJob(id, job.EraseNeeds()))
if builder.Len() > 0 {
builder.WriteString("---\n")
}
encoder := yaml.NewEncoder(builder)
encoder.SetIndent(2)
require.NoError(t, encoder.Encode(v))
}
assert.Equal(t, string(want), builder.String())
})
}
func TestParseMappingNode(t *testing.T) {
tests := []struct {
input string
scalars []string
datas []any
}{
{
input: "on:\n push:\n branches:\n - master",
scalars: []string{"push"},
datas: []any{
map[string]any{
"branches": []any{"master"},
},
},
},
{
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
scalars: []string{"branch_protection_rule"},
datas: []any{
map[string]any{
"types": []any{"created", "deleted"},
},
},
},
{
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
scalars: []string{"project", "milestone"},
datas: []any{
map[string]any{
"types": []any{"created", "deleted"},
},
map[string]any{
"types": []any{"opened", "deleted"},
},
},
},
{
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
scalars: []string{"pull_request"},
datas: []any{
map[string]any{
"types": []any{"opened"},
"branches": []any{"releases/**"},
},
},
},
{
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
scalars: []string{"push", "pull_request"},
datas: []any{
map[string]any{
"branches": []any{"main"},
},
map[string]any{
"types": []any{"opened"},
"branches": []any{"**"},
},
},
},
{
input: "on:\n schedule:\n - cron: '20 6 * * *'",
scalars: []string{"schedule"},
datas: []any{
[]any{map[string]any{
"cron": "20 6 * * *",
}},
},
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
workflow, err := model.ReadWorkflow(strings.NewReader(test.input), false)
assert.NoError(t, err)
scalars, datas, err := parseMappingNode[any](&workflow.RawOn)
assert.NoError(t, err)
assert.EqualValues(t, test.scalars, scalars, fmt.Sprintf("%#v", scalars))
assert.EqualValues(t, test.datas, datas, fmt.Sprintf("%#v", datas))
})
}
}
func TestEvaluateConcurrency(t *testing.T) {
tests := []struct {
name string
input model.RawConcurrency
group string
cancelInProgressNil bool
cancelInProgress bool
}{
{
name: "basic",
input: model.RawConcurrency{
Group: "group-name",
CancelInProgress: "true",
},
group: "group-name",
cancelInProgress: true,
},
{
name: "undefined",
input: model.RawConcurrency{},
group: "",
cancelInProgressNil: true,
},
{
name: "group-evaluation",
input: model.RawConcurrency{
Group: "${{ github.workflow }}-${{ github.ref }}",
},
group: "test_workflow-main",
cancelInProgressNil: true,
},
{
name: "cancel-evaluation-true",
input: model.RawConcurrency{
Group: "group-name",
CancelInProgress: "${{ !contains(github.ref, 'release/')}}",
},
group: "group-name",
cancelInProgress: true,
},
{
name: "cancel-evaluation-false",
input: model.RawConcurrency{
Group: "group-name",
CancelInProgress: "${{ contains(github.ref, 'release/')}}",
},
group: "group-name",
cancelInProgress: false,
},
{
name: "event-evaluation",
input: model.RawConcurrency{
Group: "user-${{ github.event.commits[0].author.username }}",
},
group: "user-someone",
cancelInProgressNil: true,
},
{
name: "arbitrary-var",
input: model.RawConcurrency{
Group: "${{ vars.eval_arbitrary_var }}",
},
group: "123",
cancelInProgressNil: true,
},
{
name: "arbitrary-input",
input: model.RawConcurrency{
Group: "${{ inputs.eval_arbitrary_input }}",
},
group: "456",
cancelInProgressNil: true,
},
{
name: "cancel-in-progress-only",
input: model.RawConcurrency{
CancelInProgress: "true",
},
group: "",
cancelInProgress: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
group, cancelInProgress, err := EvaluateWorkflowConcurrency(
&test.input,
// gitCtx
&model.GithubContext{
Workflow: "test_workflow",
Ref: "main",
Event: map[string]any{
"commits": []any{
map[string]any{
"author": map[string]any{
"username": "someone",
},
},
map[string]any{
"author": map[string]any{
"username": "someone-else",
},
},
},
},
},
// vars
map[string]string{
"eval_arbitrary_var": "123",
},
// inputs
map[string]any{
"eval_arbitrary_input": "456",
},
)
assert.NoError(t, err)
assert.EqualValues(t, test.group, group)
if test.cancelInProgressNil {
assert.Nil(t, cancelInProgress)
} else {
require.NotNil(t, cancelInProgress)
assert.EqualValues(t, test.cancelInProgress, *cancelInProgress)
}
})
}
}