Skip to content

Commit 2c73c02

Browse files
authored
feat: add gRPC loader for reading and writeing test suites (#120)
* feat: add gRPC loader for reading and writeing test suites * complete the basic feature of gRPC plugin demo * docs: add document about the gRPC extension
1 parent 8da83f3 commit 2c73c02

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5979
-366
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console/atest-ui/node_modules
2+
console/atest-ui/dist

.github/workflows/build.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ jobs:
1414
- uses: actions/[email protected]
1515
- name: Test
1616
run: |
17-
go test ./... -coverprofile coverage.out
18-
make test test-collector
17+
make test-all
1918
- name: Report
2019
if: github.actor == 'linuxsuren'
2120
env:

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
bin/
22
.idea/
33
coverage.out
4-
collector-coverage.out
4+
*-coverage.out
55
dist/
66
.vscode/launch.json
77
sample.yaml

Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
FROM golang:1.18 AS builder
22

3+
ARG GOPROXY
34
WORKDIR /workspace
45
COPY cmd/ cmd/
56
COPY pkg/ pkg/
@@ -14,9 +15,10 @@ COPY main.go main.go
1415
COPY README.md README.md
1516
COPY LICENSE LICENSE
1617

17-
RUN go mod download
18-
RUN CGO_ENABLE=0 go build -ldflags "-w -s" -o atest .
19-
RUN CGO_ENABLE=0 go build -ldflags "-w -s" -o atest-collector extensions/collector/main.go
18+
RUN GOPROXY=${GOPROXY} go mod download
19+
RUN GOPROXY=${GOPROXY} CGO_ENABLED=0 go build -ldflags "-w -s" -o atest .
20+
RUN GOPROXY=${GOPROXY} CGO_ENABLED=0 go build -ldflags "-w -s" -o atest-collector extensions/collector/main.go
21+
RUN GOPROXY=${GOPROXY} CGO_ENABLED=0 go build -ldflags "-w -s" -o atest-store-orm extensions/store-orm/main.go
2022

2123
FROM node:20-alpine3.17 AS ui
2224

@@ -40,6 +42,7 @@ LABEL "Name"="API testing"
4042

4143
COPY --from=builder /workspace/atest /usr/local/bin/atest
4244
COPY --from=builder /workspace/atest-collector /usr/local/bin/atest-collector
45+
COPY --from=builder /workspace/atest-store-orm /usr/local/bin/atest-store-orm
4346
COPY --from=builder /workspace/LICENSE /LICENSE
4447
COPY --from=builder /workspace/README.md /README.md
4548

Makefile

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
IMG_TOOL?=podman
2+
13
build:
24
mkdir -p bin
35
rm -rf bin/atest
@@ -14,9 +16,9 @@ build-embed-ui:
1416
goreleaser:
1517
goreleaser build --rm-dist --snapshot
1618
build-image:
17-
docker build -t ghcr.io/linuxsuren/api-testing:dev .
19+
${IMG_TOOL} build -t ghcr.io/linuxsuren/api-testing:master . --build-arg GOPROXY=https://goproxy.cn,direct
1820
run-image:
19-
docker run -p 7070:7070 -p 8080:8080 ghcr.io/linuxsuren/api-testing:dev
21+
docker run -p 7070:7070 -p 8080:8080 ghcr.io/linuxsuren/api-testing:master
2022
run-server:
2123
go run . server --local-storage 'sample/*.yaml' --console-path console/atest-ui/dist
2224
copy: build
@@ -25,16 +27,26 @@ copy-restart: build
2527
atest service stop
2628
make copy
2729
atest service restart
30+
2831
test:
2932
go test ./... -cover -v -coverprofile=coverage.out
3033
go tool cover -func=coverage.out
3134
test-collector:
3235
go test github.com/linuxsuren/api-testing/extensions/collector/./... -cover -v -coverprofile=collector-coverage.out
3336
go tool cover -func=collector-coverage.out
37+
test-store-orm:
38+
go test github.com/linuxsuren/api-testing/extensions/store-orm/./... -cover -v -coverprofile=store-orm-coverage.out
39+
go tool cover -func=store-orm-coverage.out
40+
test-all: test test-collector test-store-orm
41+
3442
grpc:
3543
protoc --go_out=. --go_opt=paths=source_relative \
3644
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
3745
pkg/server/server.proto
46+
47+
protoc --go_out=. --go_opt=paths=source_relative \
48+
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
49+
pkg/testing/remote/loader.proto
3850
grpc-gw:
3951
protoc -I . --grpc-gateway_out . \
4052
--grpc-gateway_opt logtostderr=true \

cmd/server.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33

44
import (
55
"bytes"
6+
"errors"
67
"fmt"
78
"log"
89
"net"
@@ -16,6 +17,7 @@ import (
1617
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
1718
"github.com/linuxsuren/api-testing/pkg/server"
1819
"github.com/linuxsuren/api-testing/pkg/testing"
20+
"github.com/linuxsuren/api-testing/pkg/testing/remote"
1921
"github.com/linuxsuren/api-testing/pkg/util"
2022
"github.com/spf13/cobra"
2123
"google.golang.org/grpc"
@@ -35,7 +37,9 @@ func createServerCmd(gRPCServer gRPCServer, httpServer server.HTTPServer) (c *co
3537
flags.IntVarP(&opt.port, "port", "p", 7070, "The RPC server port")
3638
flags.IntVarP(&opt.httpPort, "http-port", "", 8080, "The HTTP server port")
3739
flags.BoolVarP(&opt.printProto, "print-proto", "", false, "Print the proto content and exit")
40+
flags.StringVarP(&opt.storage, "storage", "", "local", "The storage type, local or etcd")
3841
flags.StringVarP(&opt.localStorage, "local-storage", "", "", "The local storage path")
42+
flags.StringVarP(&opt.grpcStorage, "grpc-storage", "", "", "The grpc storage address")
3943
flags.StringVarP(&opt.consolePath, "console-path", "", "", "The path of the console")
4044
return
4145
}
@@ -47,7 +51,9 @@ type serverOption struct {
4751
port int
4852
httpPort int
4953
printProto bool
54+
storage string
5055
localStorage string
56+
grpcStorage string
5157
consolePath string
5258
}
5359

@@ -72,11 +78,25 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
7278
return
7379
}
7480

75-
loader := testing.NewFileWriter("")
76-
if o.localStorage != "" {
77-
if err = loader.Put(o.localStorage); err != nil {
81+
var loader testing.Writer
82+
switch o.storage {
83+
case "local":
84+
loader = testing.NewFileWriter("")
85+
if o.localStorage != "" {
86+
err = loader.Put(o.localStorage)
87+
}
88+
case "grpc":
89+
if o.grpcStorage == "" {
90+
err = errors.New("grpc storage address is required")
7891
return
7992
}
93+
loader, err = remote.NewGRPCLoader(o.grpcStorage)
94+
default:
95+
err = errors.New("invalid storage type")
96+
}
97+
98+
if err != nil {
99+
return
80100
}
81101

82102
removeServer := server.NewRemoteServer(loader)

console/atest-ui/src/App.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ const handleNodeClick = (data: Tree) => {
1919
if (data.children) {
2020
viewName.value = "testsuite"
2121
testSuite.value = data.label
22+
23+
const requestOptions = {
24+
method: 'POST',
25+
body: JSON.stringify({
26+
name: data.label,
27+
})
28+
};
29+
fetch('/server.Runner/ListTestCase', requestOptions)
30+
.then(response => response.json())
31+
.then(d => {
32+
if (d.items && d.items.length > 0) {
33+
data.children=[]
34+
d.items.forEach((item: any) => {
35+
data.children?.push({
36+
id: data.label+item.name,
37+
label: item.name,
38+
parent: data.label,
39+
} as Tree)
40+
})
41+
}
42+
})
2243
} else {
2344
testCaseName.value = data.label
2445
testSuite.value = data.parent

console/atest-ui/src/views/TestCase.vue

Lines changed: 2 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
2-
import { reactive, ref, watch } from 'vue'
3-
import type { TabsPaneContext, FormInstance } from 'element-plus'
2+
import { ref, watch } from 'vue'
3+
import type { TabsPaneContext } from 'element-plus'
44
import { ElMessage } from 'element-plus'
55
import { Edit, Delete } from '@element-plus/icons-vue'
66
import JsonViewer from 'vue-json-viewer'
@@ -283,60 +283,6 @@ function headerChange() {
283283
}
284284
285285
const radio1 = ref('1')
286-
287-
const dialogVisible = ref(false)
288-
const testcaseFormRef = ref<FormInstance>()
289-
const testCaseForm = reactive({
290-
suiteName: "",
291-
name: "",
292-
api: "",
293-
})
294-
function openNewTestCaseDialog() {
295-
loadTestSuites()
296-
dialogVisible.value = true
297-
}
298-
299-
const suiteCreatingLoading = ref(false)
300-
const testSuiteList = ref([])
301-
function loadTestSuites() {
302-
const requestOptions = {
303-
method: 'POST'
304-
};
305-
fetch('/server.Runner/GetSuites', requestOptions)
306-
.then(response => response.json())
307-
.then(d => {
308-
Object.keys(d.data).map(k => {
309-
testSuiteList.value.push(k)
310-
})
311-
});
312-
}
313-
const submitForm = (formEl: FormInstance | undefined) => {
314-
if (!formEl) return
315-
suiteCreatingLoading.value = true
316-
317-
const requestOptions = {
318-
method: 'POST',
319-
body: JSON.stringify({
320-
suiteName: testCaseForm.suiteName,
321-
data: {
322-
name: testCaseForm.name,
323-
request: {
324-
api: testCaseForm.api,
325-
method: "GET",
326-
}
327-
},
328-
})
329-
};
330-
331-
fetch('/server.Runner/UpdateTestCase', requestOptions)
332-
.then(response => response.json())
333-
.then(() => {
334-
suiteCreatingLoading.value = false
335-
emit('updated', 'hello from child')
336-
});
337-
338-
dialogVisible.value = false
339-
}
340286
</script>
341287

342288
<template>
@@ -345,9 +291,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
345291
<el-header style="padding-left: 5px;">
346292
<div style="margin-bottom: 5px;">
347293
<el-button type="primary" @click="saveTestCase" :icon="Edit" :loading="saveLoading">Save</el-button>
348-
<el-button type="primary" @click="openNewTestCaseDialog" :icon="Edit">New</el-button>
349294
<el-button type="primary" @click="deleteTestCase" :icon="Delete">Delete</el-button>
350-
<el-text class="mx-1" type="primary">{{props.name}}</el-text>
351295
</div>
352296
<el-select v-model="testCaseWithSuite.data.request.method" class="m-2" placeholder="Method" size="middle">
353297
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
@@ -488,37 +432,4 @@ const submitForm = (formEl: FormInstance | undefined) => {
488432
</el-footer>
489433
</el-container>
490434
</div>
491-
492-
<el-dialog v-model="dialogVisible" title="Create Test Case" width="30%" draggable>
493-
<template #footer>
494-
<span class="dialog-footer">
495-
<el-form
496-
ref="testcaseFormRef"
497-
status-icon
498-
label-width="120px"
499-
class="demo-ruleForm"
500-
>
501-
<el-form-item label="Suite" prop="suite">
502-
<el-select class="m-2" v-model="testCaseForm.suiteName" placeholder="Select" size="large">
503-
<el-option
504-
v-for="item in testSuiteList"
505-
:key="item"
506-
:label="item"
507-
:value="item"
508-
/>
509-
</el-select>
510-
</el-form-item>
511-
<el-form-item label="Name" prop="name">
512-
<el-input v-model="testCaseForm.name" />
513-
</el-form-item>
514-
<el-form-item label="API" prop="api">
515-
<el-input v-model="testCaseForm.api" placeholder="http://foo" />
516-
</el-form-item>
517-
<el-form-item>
518-
<el-button type="primary" @click="submitForm(testcaseFormRef)" :loading="suiteCreatingLoading">Submit</el-button>
519-
</el-form-item>
520-
</el-form>
521-
</span>
522-
</template>
523-
</el-dialog>
524435
</template>

0 commit comments

Comments
 (0)