1.Add code coverage counter clear API
2.Refact core cover method and add cover unit test
This commit is contained in:
parent
7bf0db074d
commit
9795355196
49
cmd/clear.go
Normal file
49
cmd/clear.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/cover"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clearCmd = &cobra.Command{
|
||||||
|
Use: "clear",
|
||||||
|
Short: "Clear code coverage counters of all the registered services",
|
||||||
|
Long: `Clear code coverage counters for the services under test at runtime.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# clear coverage counter by special service url
|
||||||
|
goc clear --center=http://127.0.0.1:7777`,
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
res, err := cover.NewWorker().Clear(center)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("call host %v failed, err: %v, response: %v", center, err, string(res))
|
||||||
|
}
|
||||||
|
fmt.Fprint(os.Stdout, string(res))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
clearCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center")
|
||||||
|
rootCmd.AddCommand(clearCmd)
|
||||||
|
}
|
17
cmd/cover.go
17
cmd/cover.go
@ -30,6 +30,13 @@ var coverCmd = &cobra.Command{
|
|||||||
Use: "cover",
|
Use: "cover",
|
||||||
Short: "do cover for the target source ",
|
Short: "do cover for the target source ",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if mode == "" {
|
||||||
|
log.Fatalf("Error: flag needs an argument: -mode %v", mode)
|
||||||
|
}
|
||||||
|
if mode != "set" && mode != "count" && mode != "atomic" {
|
||||||
|
log.Fatalf("unknown -mode %v", mode)
|
||||||
|
}
|
||||||
|
|
||||||
doCover(cmd, args, "", "")
|
doCover(cmd, args, "", "")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -37,11 +44,13 @@ var coverCmd = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
target string
|
target string
|
||||||
center string
|
center string
|
||||||
|
mode string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
coverCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center")
|
coverCmd.Flags().StringVarP(¢er, "center", "", "http://127.0.0.1:7777", "cover profile host center")
|
||||||
coverCmd.Flags().StringVarP(&target, "target", "", ".", "target folder to cover")
|
coverCmd.Flags().StringVarP(&target, "target", "", ".", "target folder to cover")
|
||||||
|
coverCmd.Flags().StringVarP(&mode, "mode", "", "count", "coverage mode: set, count, atomic")
|
||||||
|
|
||||||
rootCmd.AddCommand(coverCmd)
|
rootCmd.AddCommand(coverCmd)
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
@ -68,14 +77,14 @@ func doCover(cmd *cobra.Command, args []string, newgopath string, newtarget stri
|
|||||||
if pkg.Name == "main" {
|
if pkg.Name == "main" {
|
||||||
log.Printf("handle package: %v", pkg.ImportPath)
|
log.Printf("handle package: %v", pkg.ImportPath)
|
||||||
// inject the main package
|
// inject the main package
|
||||||
mainCover, err := cover.AddCounters(pkg, newgopath)
|
mainCover, err := cover.AddCounters(pkg, mode, newgopath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to add counters for pkg %s, err: %v", pkg.ImportPath, err)
|
log.Fatalf("failed to add counters for pkg %s, err: %v", pkg.ImportPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// new a testcover for this service
|
// new a testcover for this service
|
||||||
tc := cover.TestCover{
|
tc := cover.TestCover{
|
||||||
Mode: "atomic",
|
Mode: mode,
|
||||||
Center: center,
|
Center: center,
|
||||||
MainPkgCover: mainCover,
|
MainPkgCover: mainCover,
|
||||||
}
|
}
|
||||||
@ -103,7 +112,7 @@ func doCover(cmd *cobra.Command, args []string, newgopath string, newtarget stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add counter for internal package
|
// add counter for internal package
|
||||||
inPkgCover, err := cover.AddCounters(depPkg, newgopath)
|
inPkgCover, err := cover.AddCounters(depPkg, mode, newgopath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to add counters for internal pkg %s, err: %v", depPkg.ImportPath, err)
|
log.Fatalf("failed to add counters for internal pkg %s, err: %v", depPkg.ImportPath, err)
|
||||||
}
|
}
|
||||||
@ -149,7 +158,7 @@ func doCover(cmd *cobra.Command, args []string, newgopath string, newtarget stri
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
packageCover, err := cover.AddCounters(depPkg, newgopath)
|
packageCover, err := cover.AddCounters(depPkg, mode, newgopath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to add counters for pkg %s, err: %v", depPkg.ImportPath, err)
|
log.Fatalf("failed to add counters for pkg %s, err: %v", depPkg.ImportPath, err)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import (
|
|||||||
// Action provides methods to contact with the coverd service under test
|
// Action provides methods to contact with the coverd service under test
|
||||||
type Action interface {
|
type Action interface {
|
||||||
Profile(host string) ([]byte, error)
|
Profile(host string) ([]byte, error)
|
||||||
Clear() error
|
Clear(host string) ([]byte, error)
|
||||||
InitSystem(host string) ([]byte, error)
|
InitSystem(host string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +61,14 @@ func (w *client) Profile(host string) ([]byte, error) {
|
|||||||
return profile, err
|
return profile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *client) Clear() error { return nil }
|
func (w *client) Clear(host string) ([]byte, error) {
|
||||||
|
u := fmt.Sprintf("%s%s", host, CoverProfileClearAPI)
|
||||||
|
resp, err := w.do("POST", u, nil)
|
||||||
|
if err != nil && isNetworkError(err) {
|
||||||
|
resp, err = w.do("POST", u, nil)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (w *client) InitSystem(host string) ([]byte, error) {
|
func (w *client) InitSystem(host string) ([]byte, error) {
|
||||||
u := fmt.Sprintf("%s%s", host, CoverInitSystemAPI)
|
u := fmt.Sprintf("%s%s", host, CoverInitSystemAPI)
|
||||||
|
@ -145,23 +145,14 @@ func ListPackages(dir string, args []string, newgopath string) map[string]*Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddCounters add counters for all go files under the package
|
// AddCounters add counters for all go files under the package
|
||||||
func AddCounters(pkg *Package, newgopath string) (*PackageCover, error) {
|
func AddCounters(pkg *Package, mode, newgopath string) (*PackageCover, error) {
|
||||||
coverVarMap := declareCoverVars(pkg)
|
coverVarMap := declareCoverVars(pkg)
|
||||||
|
|
||||||
// to construct: go tool cover -mode=atomic -o dest src (note: dest==src)
|
|
||||||
var args = []string{"tool", "cover", "-mode=atomic"}
|
|
||||||
for file, coverVar := range coverVarMap {
|
for file, coverVar := range coverVarMap {
|
||||||
var newArgs = args
|
cmd := buildCoverCmd(file, coverVar, pkg, mode, newgopath)
|
||||||
newArgs = append(newArgs, "-var", coverVar.Var)
|
|
||||||
longPath := path.Join(pkg.Dir, file)
|
|
||||||
newArgs = append(newArgs, "-o", longPath, longPath)
|
|
||||||
cmd := exec.Command("go", newArgs...)
|
|
||||||
if newgopath != "" {
|
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", newgopath))
|
|
||||||
}
|
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("execuate go tool cover -mode=atomic -var %s -o %s %s failed, err: %v, out: %s", coverVar.Var, longPath, longPath, err, string(out))
|
return nil, fmt.Errorf("execuate go tool cover -mode=atomic -var %s -o %s/%s failed, err: %v, out: %s", coverVar.Var, pkg.Dir, file, err, string(out))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,6 +162,20 @@ func AddCounters(pkg *Package, newgopath string) (*PackageCover, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildCoverCmd(file string, coverVar *FileVar, pkg *Package, mode, newgopath string) *exec.Cmd {
|
||||||
|
// to construct: go tool cover -mode=atomic -o dest src (note: dest==src)
|
||||||
|
var newArgs = []string{"tool", "cover"}
|
||||||
|
newArgs = append(newArgs, "-mode", mode)
|
||||||
|
newArgs = append(newArgs, "-var", coverVar.Var)
|
||||||
|
longPath := path.Join(pkg.Dir, file)
|
||||||
|
newArgs = append(newArgs, "-o", longPath, longPath)
|
||||||
|
cmd := exec.Command("go", newArgs...)
|
||||||
|
if newgopath != "" {
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("GOPATH=%v", newgopath))
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
// declareCoverVars attaches the required cover variables names
|
// declareCoverVars attaches the required cover variables names
|
||||||
// to the files, to be used when annotating the files.
|
// to the files, to be used when annotating the files.
|
||||||
func declareCoverVars(p *Package) map[string]*FileVar {
|
func declareCoverVars(p *Package) map[string]*FileVar {
|
||||||
|
@ -17,6 +17,12 @@
|
|||||||
package cover
|
package cover
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -95,3 +101,114 @@ func TestCovList(t *testing.T) {
|
|||||||
assert.Equal(t, "100.0%", covF.Percentage())
|
assert.Equal(t, "100.0%", covF.Percentage())
|
||||||
assert.Equal(t, "0.0%", covF1.Percentage())
|
assert.Equal(t, "0.0%", covF1.Percentage())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCoverCmd(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
coverVar *FileVar
|
||||||
|
pkg *Package
|
||||||
|
mode string
|
||||||
|
newgopath string
|
||||||
|
expectCmd *exec.Cmd
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
file: "c.go",
|
||||||
|
coverVar: &FileVar{
|
||||||
|
File: "example/b/c/c.go",
|
||||||
|
Var: "GoCover_0_643131623532653536333031",
|
||||||
|
},
|
||||||
|
pkg: &Package{
|
||||||
|
Dir: "/go/src/goc/cmd/example-project/b/c",
|
||||||
|
},
|
||||||
|
mode: "count",
|
||||||
|
newgopath: "",
|
||||||
|
expectCmd: &exec.Cmd{
|
||||||
|
Path: lookCmdPath("go"),
|
||||||
|
Args: []string{"go", "tool", "cover", "-mode", "count", "-var", "GoCover_0_643131623532653536333031", "-o",
|
||||||
|
"/go/src/goc/cmd/example-project/b/c/c.go", "/go/src/goc/cmd/example-project/b/c/c.go"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal with gopath",
|
||||||
|
file: "c.go",
|
||||||
|
coverVar: &FileVar{
|
||||||
|
File: "example/b/c/c.go",
|
||||||
|
Var: "GoCover_0_643131623532653536333031",
|
||||||
|
},
|
||||||
|
pkg: &Package{
|
||||||
|
Dir: "/go/src/goc/cmd/example-project/b/c",
|
||||||
|
},
|
||||||
|
mode: "set",
|
||||||
|
newgopath: "/go/src/goc",
|
||||||
|
expectCmd: &exec.Cmd{
|
||||||
|
Path: lookCmdPath("go"),
|
||||||
|
Args: []string{"go", "tool", "cover", "-mode", "set", "-var", "GoCover_0_643131623532653536333031", "-o",
|
||||||
|
"/go/src/goc/cmd/example-project/b/c/c.go", "/go/src/goc/cmd/example-project/b/c/c.go"},
|
||||||
|
Env: append(os.Environ(), fmt.Sprintf("GOPATH=%v", "/go/src/goc")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
cmd := buildCoverCmd(testCase.file, testCase.coverVar, testCase.pkg, testCase.mode, testCase.newgopath)
|
||||||
|
if !reflect.DeepEqual(cmd, testCase.expectCmd) {
|
||||||
|
t.Errorf("generated incorrect commands:\nGot: %#v\nExpected:%#v", cmd, testCase.expectCmd)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookCmdPath(name string) string {
|
||||||
|
if filepath.Base(name) == name {
|
||||||
|
if lp, err := exec.LookPath(name); err != nil {
|
||||||
|
log.Fatalf("find exec %s err: %v", name, err)
|
||||||
|
} else {
|
||||||
|
return lp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeclareCoverVars(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
pkg *Package
|
||||||
|
expectCoverVar map[string]*FileVar
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
pkg: &Package{
|
||||||
|
Dir: "/go/src/goc/cmd/example-project/b/c",
|
||||||
|
GoFiles: []string{"c.go"},
|
||||||
|
ImportPath: "example/b/c",
|
||||||
|
},
|
||||||
|
expectCoverVar: map[string]*FileVar{
|
||||||
|
"c.go": {File: "example/b/c/c.go", Var: "GoCover_0_643131623532653536333031"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "more go files",
|
||||||
|
pkg: &Package{
|
||||||
|
Dir: "/go/src/goc/cmd/example-project/a/b",
|
||||||
|
GoFiles: []string{"printf.go", "printf1.go"},
|
||||||
|
ImportPath: "example/a/b",
|
||||||
|
},
|
||||||
|
expectCoverVar: map[string]*FileVar{
|
||||||
|
"printf.go": {File: "example/a/b/printf.go", Var: "GoCover_0_326535623364613565313464"},
|
||||||
|
"printf1.go": {File: "example/a/b/printf1.go", Var: "GoCover_1_326535623364613565313464"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
coverVar := declareCoverVars(testCase.pkg)
|
||||||
|
if !reflect.DeepEqual(coverVar, testCase.expectCoverVar) {
|
||||||
|
t.Errorf("generated incorrect cover vars:\nGot: %#v\nExpected:%#v", coverVar, testCase.expectCoverVar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -116,6 +116,32 @@ func loadFileCover(coverCounters map[string][]uint32, coverBlocks map[string][]t
|
|||||||
coverBlocks[fileName] = block
|
coverBlocks[fileName] = block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearValues() {
|
||||||
|
|
||||||
|
{{range $i, $pkgCover := .DepsCover}}
|
||||||
|
{{range $file, $cover := $pkgCover.Vars}}
|
||||||
|
clearFileCover(_cover{{$i}}.{{$cover.Var}}.Count[:])
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range $file, $cover := .MainPkgCover.Vars}}
|
||||||
|
clearFileCover({{$cover.Var}}.Count[:])
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range $k, $pkgCover := .CacheCover}}
|
||||||
|
{{range $v, $cover := $pkgCover.Vars}}
|
||||||
|
clearFileCover({{$pkgCover.Package.Name}}.{{$v}}.Count[:])
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearFileCover(counter []uint32) {
|
||||||
|
for i := range counter {
|
||||||
|
counter[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func registerHandlers() {
|
func registerHandlers() {
|
||||||
ln, host, err := listen()
|
ln, host, err := listen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -127,13 +153,11 @@ func registerHandlers() {
|
|||||||
log.Fatalf("register address %v failed, err: %v, response: %v", profileAddr, err, string(resp))
|
log.Fatalf("register address %v failed, err: %v, response: %v", profileAddr, err, string(resp))
|
||||||
}
|
}
|
||||||
go genProfileAddr(host)
|
go genProfileAddr(host)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
// Coverage reports the current code coverage as a fraction in the range [0, 1].
|
// Coverage reports the current code coverage as a fraction in the range [0, 1].
|
||||||
// If coverage is not enabled, Coverage returns 0.
|
// If coverage is not enabled, Coverage returns 0.
|
||||||
mux.HandleFunc("/v1/cover/coverage", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/v1/cover/coverage", func(w http.ResponseWriter, r *http.Request) {
|
||||||
counters, _ := loadValues()
|
counters, _ := loadValues()
|
||||||
|
|
||||||
var n, d int64
|
var n, d int64
|
||||||
for _, counter := range counters {
|
for _, counter := range counters {
|
||||||
for i := range counter {
|
for i := range counter {
|
||||||
@ -152,9 +176,8 @@ func registerHandlers() {
|
|||||||
|
|
||||||
// coverprofile reports a coverage profile with the coverage percentage
|
// coverprofile reports a coverage profile with the coverage percentage
|
||||||
mux.HandleFunc("/v1/cover/profile", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/v1/cover/profile", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, "mode: atomic\n")
|
fmt.Fprint(w, "mode: {{.Mode }} \n")
|
||||||
counters, blocks := loadValues()
|
counters, blocks := loadValues()
|
||||||
|
|
||||||
var active, total int64
|
var active, total int64
|
||||||
var count uint32
|
var count uint32
|
||||||
for name, counts := range counters {
|
for name, counts := range counters {
|
||||||
@ -180,7 +203,9 @@ func registerHandlers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mux.HandleFunc("/v1/cover/clear", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/v1/cover/clear", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, "TO BE IMPLEMENTED!\n")
|
clearValues()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintln(w,"clear call successfully")
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Fatal(http.Serve(ln, mux))
|
log.Fatal(http.Serve(ln, mux))
|
||||||
|
@ -142,16 +142,17 @@ func profile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func clear(c *gin.Context) {
|
func clear(c *gin.Context) {
|
||||||
if err := LocalStore.Init(); err != nil {
|
svrsUnderTest := LocalStore.GetAll()
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
for svc, addrs := range svrsUnderTest {
|
||||||
return
|
for _, addr := range addrs {
|
||||||
|
pp, err := Client.Clear(addr)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(c.Writer, "Register service %s: %s coverage counter %s", svc, addr, string(pp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Client.Clear(); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, "TO BE IMPLEMENTED")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSystem(c *gin.Context) {
|
func initSystem(c *gin.Context) {
|
||||||
|
Loading…
Reference in New Issue
Block a user