Merge pull request #726 from jamiehannaford/add-tests

Add generator tests
This commit is contained in:
Christoph Blecker 2017-07-14 10:13:35 -07:00 committed by GitHub
commit 789f252a71
16 changed files with 436 additions and 133 deletions

View File

@ -6,4 +6,5 @@ services:
- docker
script:
- make test-unit
- bash ./scripts/verify.sh

View File

@ -1,3 +1,5 @@
IMAGE_NAME=kube-communitydocs
all: \
build-image \
gen-docs \
@ -6,10 +8,10 @@ reset-docs:
git checkout HEAD -- sig-list.md sig-*
build-image:
docker build -t sigdocs -f generator/Dockerfile generator
gen-doc:
docker run -e WG=${WG} -e SIG=${SIG} -v $(shell pwd):/go/src/app sigdocs
docker build -t $(IMAGE_NAME) -f generator/Dockerfile generator
gen-docs:
docker run -v $(shell pwd):/go/src/app sigdocs
docker run --rm -e WG -e SIG -v $(shell pwd):/go/src/app/generated $(IMAGE_NAME) app
test: build-image
docker run --rm $(IMAGE_NAME) go test -v ./...

1
generator/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
generated/

View File

@ -1 +1,7 @@
FROM golang:1.6-onbuild
FROM golang:1.8
WORKDIR /go/src/app
COPY . .
RUN go-wrapper download
RUN go-wrapper install

View File

@ -1,14 +1,26 @@
# SIG Doc builder
This script will generate the following documentation files:
This folder contains scripts to automatically generate documentation about the
different Special Interest Groups (SIGs) of Kubernetes. The authoritative
source for SIG information is the `sigs.yaml` file in the project root. All
updates must be done there.
When an update happens to the this file, the next step is generate the
accompanying documentation. This takes the format of two types of doc file:
```
sig-*/README.md
wg-*/README.md
sig-list.md
./sig-<sig-name>/README.md
./wg-<working-group-name>/README.md
./sig-list.md
```
Based off the `sigs.yaml` metadata file.
For example, if a contributor has updated `sig-cluster-lifecycle`, the
following files will be generated:
```
./sig-cluster-lifecycle/README.md
./sig-list.md
```
## How to use
@ -21,9 +33,9 @@ make all
To build docs for one SIG, run these commands:
```bash
make SIG=sig-apps gen-doc
make SIG=sig-testing gen-doc
make WG=resource-management gen-doc
make SIG=sig-apps gen-docs
make SIG=sig-testing gen-docs
make WG=resource-management gen-docs
```
where the `SIG` or `WG` var refers to the directory being built.

View File

@ -30,25 +30,29 @@ import (
)
var (
sigsYamlFile = "sigs.yaml"
templateDir = "generator"
sigIndexTemplate = filepath.Join(templateDir, "sig_index.tmpl")
wgIndexTemplate = filepath.Join(templateDir, "wg_index.tmpl")
listTemplate = filepath.Join(templateDir, "sig_list.tmpl")
headerTemplate = filepath.Join(templateDir, "header.tmpl")
sigListOutput = "sig-list.md"
sigIndexOutput = "README.md"
githubTeamNames = []string{"misc", "test-failures", "bugs", "feature-requests", "proposals", "pr-reviews", "api-reviews"}
beginMarker = "<!-- BEGIN CUSTOM CONTENT -->"
endMarker = "<!-- END CUSTOM CONTENT -->"
readmeTemplate = "readme.tmpl"
listTemplate = "list.tmpl"
headerTemplate = "header.tmpl"
sigsYamlFile = "sigs.yaml"
sigListOutput = "sig-list.md"
indexFilename = "README.md"
baseOutputDir = "generated"
githubTeamNames = []string{"misc", "test-failures", "bugs", "feature-requests", "proposals", "pr-reviews", "api-reviews"}
beginMarker = "<!-- BEGIN CUSTOM CONTENT -->"
endMarker = "<!-- END CUSTOM CONTENT -->"
)
// Lead represents a lead engineer for a particular group. There are usually
// 2 per group.
type Lead struct {
Name string
Company string
GitHub string
}
// Meeting represents a regular meeting for a group.
type Meeting struct {
Day string
UTC string
@ -56,6 +60,7 @@ type Meeting struct {
Frequency string
}
// Contact represents the various contact points for a group.
type Contact struct {
Slack string
MailingList string `yaml:"mailing_list"`
@ -64,7 +69,8 @@ type Contact struct {
GithubTeamNames []string
}
type Sig struct {
// Group represents either a Special Interest Group (SIG) or a Working Group (WG)
type Group struct {
Name string
Dir string
MissionStatement string `yaml:"mission_statement"`
@ -75,52 +81,31 @@ type Sig struct {
Contact Contact
}
type Wg struct {
Name string
Dir string
MissionStatement string `yaml:"mission_statement"`
Organizers []Lead
Meetings []Meeting
MeetingURL string `yaml:"meeting_url"`
MeetingArchiveURL string `yaml:"meeting_archive_url"`
Contact Contact
// DirName returns the directory that a group's documentation will be
// generated into. It is composed of a prefix (sig for SIGs and wg for WGs),
// and a formatted version of the group's name (in kebab case).
func (e *Group) DirName(prefix string) string {
return fmt.Sprintf("%s-%s", prefix, strings.ToLower(strings.Replace(e.Name, " ", "-", -1)))
}
// SetupGitHubTeams will iterate over all the possible teams available to a
// group (these are defined by the Kubernetes organisation) and populate a
// list using the group's prefix.
func (e *Group) SetupGitHubTeams(prefix string) {
ghPrefix := e.Contact.GithubTeamPrefix
if ghPrefix == "" {
ghPrefix = e.DirName(prefix)
}
for _, gtn := range githubTeamNames {
e.Contact.GithubTeamNames = append(e.Contact.GithubTeamNames, fmt.Sprintf("%s-%s", ghPrefix, gtn))
}
}
// Context is the context for the sigs.yaml file.
type Context struct {
Sigs []Sig
WorkingGroups []Wg
}
type SigEntries struct {
Sigs []Sig
}
func (slice SigEntries) Len() int {
return len(slice.Sigs)
}
func (slice SigEntries) Less(i, j int) bool {
return slice.Sigs[i].Name < slice.Sigs[j].Name
}
func (slice SigEntries) Swap(i, j int) {
slice.Sigs[i], slice.Sigs[j] = slice.Sigs[j], slice.Sigs[i]
}
type WgEntries struct {
WorkingGroups []Wg
}
func (slice WgEntries) Len() int {
return len(slice.WorkingGroups)
}
func (slice WgEntries) Less(i, j int) bool {
return slice.WorkingGroups[i].Name < slice.WorkingGroups[j].Name
}
func (slice WgEntries) Swap(i, j int) {
slice.WorkingGroups[i], slice.WorkingGroups[j] = slice.WorkingGroups[j], slice.WorkingGroups[i]
Sigs []Group
WorkingGroups []Group
}
func pathExists(path string) bool {
@ -130,8 +115,7 @@ func pathExists(path string) bool {
func createDirIfNotExists(path string) error {
if !pathExists(path) {
fmt.Printf("%s directory does not exist, creating\n", path)
return os.Mkdir(path, 0755)
return os.MkdirAll(path, 0755)
}
return nil
}
@ -171,7 +155,7 @@ func writeTemplate(templatePath, outputPath string, data interface{}) error {
// create if not exists
if !pathExists(outputPath) {
_, err := os.Create(outputPath)
_, err = os.Create(outputPath)
if err != nil {
return err
}
@ -202,7 +186,6 @@ func writeTemplate(templatePath, outputPath string, data interface{}) error {
// custom content block
writeCustomContentBlock(f, content)
fmt.Printf("Generated %s\n", outputPath)
return nil
}
@ -213,60 +196,33 @@ func writeCustomContentBlock(f *os.File, content string) {
}
}
func createReadmeFiles(ctx Context) error {
var selectedSig *string
if sig, ok := os.LookupEnv("SIG"); ok {
selectedSig = &sig
func createGroupReadme(groups []Group, prefix string) error {
// figure out if the user wants to generate one group
var selectedGroupName *string
if envVal, ok := os.LookupEnv(strings.ToUpper(prefix)); ok {
selectedGroupName = &envVal
}
for _, sig := range ctx.Sigs {
dirName := fmt.Sprintf("sig-%s", strings.ToLower(strings.Replace(sig.Name, " ", "-", -1)))
if selectedSig != nil && *selectedSig != dirName {
fmt.Printf("Skipping %s\n", dirName)
for _, group := range groups {
group.Dir = group.DirName(prefix)
// skip generation if the user specified only one group
if selectedGroupName != nil && *selectedGroupName != group.Dir {
fmt.Printf("Skipping %s/README.md\n", group.Dir)
continue
}
createDirIfNotExists(dirName)
fmt.Printf("Generating %s/README.md\n", group.Dir)
prefix := sig.Contact.GithubTeamPrefix
if prefix == "" {
prefix = dirName
}
for _, gtn := range githubTeamNames {
sig.Contact.GithubTeamNames = append(sig.Contact.GithubTeamNames, fmt.Sprintf("%s-%s", prefix, gtn))
}
outputPath := fmt.Sprintf("%s/%s", dirName, sigIndexOutput)
if err := writeTemplate(sigIndexTemplate, outputPath, sig); err != nil {
outputDir := filepath.Join(baseOutputDir, group.Dir)
if err := createDirIfNotExists(outputDir); err != nil {
return err
}
}
var selectedWg *string
if wg, ok := os.LookupEnv("WG"); ok {
selectedWg = &wg
}
for _, wg := range ctx.WorkingGroups {
dirName := fmt.Sprintf("wg-%s", strings.ToLower(strings.Replace(wg.Name, " ", "-", -1)))
if selectedWg != nil && *selectedWg != dirName {
fmt.Printf("Skipping %s\n", dirName)
continue
}
group.SetupGitHubTeams(prefix)
createDirIfNotExists(dirName)
prefix := wg.Contact.GithubTeamPrefix
if prefix == "" {
prefix = dirName
}
for _, gtn := range githubTeamNames {
wg.Contact.GithubTeamNames = append(wg.Contact.GithubTeamNames, fmt.Sprintf("%s-%s", prefix, gtn))
}
outputPath := fmt.Sprintf("%s/%s", dirName, sigIndexOutput)
if err := writeTemplate(wgIndexTemplate, outputPath, wg); err != nil {
outputPath := filepath.Join(outputDir, indexFilename)
readmePath := fmt.Sprintf("%s_%s", prefix, readmeTemplate)
if err := writeTemplate(readmePath, outputPath, group); err != nil {
return err
}
}
@ -274,12 +230,8 @@ func createReadmeFiles(ctx Context) error {
return nil
}
func createListFile(ctx Context) error {
return writeTemplate(listTemplate, sigListOutput, ctx)
}
func main() {
yamlData, err := ioutil.ReadFile(sigsYamlFile)
yamlData, err := ioutil.ReadFile(filepath.Join(baseOutputDir, sigsYamlFile))
if err != nil {
log.Fatal(err)
}
@ -289,19 +241,28 @@ func main() {
if err != nil {
log.Fatal(err)
}
sigEntries := SigEntries{ctx.Sigs}
sort.Sort(sigEntries)
ctx.Sigs = sigEntries.Sigs
wgEntries := WgEntries{ctx.WorkingGroups}
sort.Sort(wgEntries)
ctx.WorkingGroups = wgEntries.WorkingGroups
err = createReadmeFiles(ctx)
sort.Slice(ctx.Sigs, func(i, j int) bool {
return ctx.Sigs[i].Name <= ctx.Sigs[j].Name
})
sort.Slice(ctx.WorkingGroups, func(i, j int) bool {
return ctx.WorkingGroups[i].Name <= ctx.WorkingGroups[j].Name
})
err = createGroupReadme(ctx.Sigs, "sig")
if err != nil {
log.Fatal(err)
}
err = createListFile(ctx)
err = createGroupReadme(ctx.WorkingGroups, "wg")
if err != nil {
log.Fatal(err)
}
fmt.Println("Generating sig-list.md")
outputPath := filepath.Join(baseOutputDir, sigListOutput)
err = writeTemplate(listTemplate, outputPath, ctx)
if err != nil {
log.Fatal(err)
}

249
generator/app_test.go Normal file
View File

@ -0,0 +1,249 @@
/*
Copyright 2017 The Kubernetes Authors.
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 main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestNonExistantDirIsCreated(t *testing.T) {
dir := "/tmp/nonexistent"
err := createDirIfNotExists(dir)
if err != nil {
t.Fatalf("Received error creating dir: %v", err)
}
if !pathExists(dir) {
t.Fatalf("%s should exist", dir)
}
}
func TestExistantDirNotCreated(t *testing.T) {
dir := "./testdata"
err := createDirIfNotExists(dir)
if err != nil {
t.Fatalf("Received error creating dir: %v", err)
}
}
func TestGetExistingData(t *testing.T) {
cases := []struct {
path string
expected string
expectErr bool
}{
{
path: "./testdata/custom_content.md",
expected: "FOO BAR BAZ",
expectErr: false,
},
{
path: "./testdata/no_custom_content.md",
expected: "",
expectErr: false,
},
{
path: "./testdata/foo.md",
expected: "",
expectErr: true,
},
}
for _, c := range cases {
content, err := getExistingContent(c.path)
if err != nil && c.expectErr == false {
t.Fatalf("Received unexpected error for %s: %v", c.path, err)
}
if err == nil && c.expectErr == true {
t.Fatalf("Expected error for %s but received none", c.path)
}
if content != c.expected {
t.Fatalf("Expected %s but received %s", c.expected, content)
}
}
}
func TestWriteTemplate(t *testing.T) {
customContent := `
<!-- BEGIN CUSTOM CONTENT -->
Example
custom
content!
<!-- END CUSTOM CONTENT -->
`
cases := []struct {
templatePath string
outputPath string
data map[string]string
expectErr bool
expected string
}{
{
templatePath: "./testdata/non_existent_template.tmpl",
expectErr: true,
},
{
templatePath: "./testdata/example.tmpl",
outputPath: "/tmp/non_existing_path.md",
expectErr: false,
data: map[string]string{"Message": "Hello!"},
expected: "Hello!",
},
{
templatePath: "./testdata/example.tmpl",
outputPath: "./testdata/example.md",
expectErr: false,
data: map[string]string{"Message": "Hello!"},
expected: customContent,
},
}
for _, c := range cases {
err := writeTemplate(c.templatePath, c.outputPath, c.data)
if err != nil && c.expectErr == false {
t.Fatalf("Received unexpected error for %s: %v", c.templatePath, err)
}
if c.expectErr {
if err == nil {
t.Fatalf("Expected error for %s but received none", c.templatePath)
}
continue
}
content, err := ioutil.ReadFile(c.outputPath)
if err != nil {
t.Fatalf("%s should exist", c.outputPath)
}
if strings.Contains(string(content), c.expected) == false {
t.Fatalf("%s was not found in %s", c.expected, c.outputPath)
}
}
}
func TestGroupDirName(t *testing.T) {
group := Group{Name: "Foo Bar"}
if group.DirName("sig") != "sig-foo-bar" {
t.Fatal("DirName incorrect")
}
}
func TestSetupGithubTeams(t *testing.T) {
group := Group{Name: "Foo Bar"}
group.SetupGitHubTeams("sig")
var expected []string
for _, ght := range githubTeamNames {
expected = append(expected, fmt.Sprintf("sig-foo-bar-%s", ght))
}
if !reflect.DeepEqual(group.Contact.GithubTeamNames, expected) {
t.Fatalf("%v does not match %v", group.Contact.GithubTeamNames, expected)
}
}
func TestCustomPrefixSetupGithubTeams(t *testing.T) {
group := Group{Contact: Contact{GithubTeamPrefix: "foo"}}
group.SetupGitHubTeams("")
var expected []string
for _, ght := range githubTeamNames {
expected = append(expected, fmt.Sprintf("foo-%s", ght))
}
if !reflect.DeepEqual(group.Contact.GithubTeamNames, expected) {
t.Fatalf("%v does not match %v", group.Contact.GithubTeamNames, expected)
}
}
func TestCreateGroupReadmes(t *testing.T) {
groups := []Group{
Group{Name: "Foo"},
Group{Name: "Bar"},
}
err := createGroupReadme(groups, "sig")
if err != nil {
t.Fatal(err)
}
for _, group := range groups {
path := filepath.Join(baseOutputDir, group.DirName("sig"), "README.md")
if !pathExists(path) {
t.Fatalf("%s should exist", path)
}
}
}
func TestReadmesAreSkipped(t *testing.T) {
os.Setenv("SIG", "sig-foo")
groups := []Group{
Group{Name: "Foo"},
Group{Name: "Bar"},
}
err := createGroupReadme(groups, "sig")
if err != nil {
t.Fatal(err)
}
for _, group := range groups[1:] {
path := filepath.Join(baseOutputDir, group.DirName("sig"), "README.md")
if !pathExists(path) {
t.Fatalf("%s should exist", path)
}
}
os.Setenv("SIG", "")
}
func copyFile(src, dst string) error {
// Read all content of src to data
data, err := ioutil.ReadFile(src)
if err != nil {
return err
}
// Write data to dst
err = ioutil.WriteFile(dst, data, 0644)
if err != nil {
return err
}
return nil
}
func TestFullGeneration(t *testing.T) {
err := copyFile("testdata/sigs.yaml", "generated/sigs.yaml")
if err != nil {
t.Fatalf("Error received: %v", err)
}
main()
expectedDirs := []string{"sig-foo", "sig-bar", "wg-baz"}
for _, ed := range expectedDirs {
path := filepath.Join(baseOutputDir, ed, "README.md")
if !pathExists(path) {
t.Fatalf("%s should exist", path)
}
}
}

View File

@ -24,5 +24,5 @@ When the need arises, a [new SIG can be created](sig-creation-procedure.md)
| Name | Organizers | Contact | Meetings |
|------|------------|---------|----------|
{{- range .WorkingGroups}}
|[{{.Name}}]({{.Dir}}/README.md)|{{range .Organizers}}* [{{.Name}}](https://github.com/{{.GitHub}}){{if .Company}}, {{.Company}}{{end}}<br>{{end}}|* [Slack](https://kubernetes.slack.com/messages/{{.Contact.Slack}})<br>* [Mailing List]({{.Contact.MailingList}})|{{ $save := . }}{{range .Meetings}}* [{{.Day}}s at {{.UTC}} UTC ({{.Frequency}})]({{$save.MeetingURL}})<br>{{end}}
|[{{.Name}}]({{.Dir}}/README.md)|{{range .Leads}}* [{{.Name}}](https://github.com/{{.GitHub}}){{if .Company}}, {{.Company}}{{end}}<br>{{end}}|* [Slack](https://kubernetes.slack.com/messages/{{.Contact.Slack}})<br>* [Mailing List]({{.Contact.MailingList}})|{{ $save := . }}{{range .Meetings}}* [{{.Day}}s at {{.UTC}} UTC ({{.Frequency}})]({{$save.MeetingURL}})<br>{{end}}
{{- end }}

29
generator/testdata/custom_content.md vendored Normal file
View File

@ -0,0 +1,29 @@
<!---
This is an autogenerated file!
Please do not edit this file directly, but instead make changes to the
sigs.yaml file in the project root.
To understand how this file is generated, see generator/README.md.
-->
# Auth SIG
Covers improvements to Kubernetes authorization, authentication, and cluster security policy.
## Meetings
* [Wednesdays at 18:00 UTC](https://zoom.us/my/k8s.sig.auth) (biweekly). [Convert to your timezone](http://www.thetimezoneconverter.com/?t=18:00&tz=UTC).
Meeting notes and Agenda can be found [here](https://docs.google.com/document/d/1woLGRoONE3EBVx-wTb4pvp4CI7tmLZ6lS26VTbosLKM/edit#).
## Leads
* [Eric Chiang](https://github.com/ericchiang), CoreOS
* [Jordan Liggitt](https://github.com/liggitt), Red Hat
* [David Eads](https://github.com/deads2k), Red Hat
## Contact
* [Slack](https://kubernetes.slack.com/messages/sig-auth)
* [Mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-auth)
<!-- BEGIN CUSTOM CONTENT -->
FOO BAR BAZ
<!-- END CUSTOM CONTENT -->

7
generator/testdata/example.md vendored Normal file
View File

@ -0,0 +1,7 @@
Hello!
<!-- BEGIN CUSTOM CONTENT -->
Example
custom
content!
<!-- END CUSTOM CONTENT -->

1
generator/testdata/example.tmpl vendored Normal file
View File

@ -0,0 +1 @@
{{.Message}}

29
generator/testdata/no_custom_content.md vendored Normal file
View File

@ -0,0 +1,29 @@
<!---
This is an autogenerated file!
Please do not edit this file directly, but instead make changes to the
sigs.yaml file in the project root.
To understand how this file is generated, see generator/README.md.
-->
# Auth SIG
Covers improvements to Kubernetes authorization, authentication, and cluster security policy.
## Meetings
* [Wednesdays at 18:00 UTC](https://zoom.us/my/k8s.sig.auth) (biweekly). [Convert to your timezone](http://www.thetimezoneconverter.com/?t=18:00&tz=UTC).
Meeting notes and Agenda can be found [here](https://docs.google.com/document/d/1woLGRoONE3EBVx-wTb4pvp4CI7tmLZ6lS26VTbosLKM/edit#).
## Leads
* [Eric Chiang](https://github.com/ericchiang), CoreOS
* [Jordan Liggitt](https://github.com/liggitt), Red Hat
* [David Eads](https://github.com/deads2k), Red Hat
## Contact
* [Slack](https://kubernetes.slack.com/messages/sig-auth)
* [Mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-auth)
<!-- BEGIN CUSTOM CONTENT -->
<!-- END CUSTOM CONTENT -->

5
generator/testdata/sigs.yaml vendored Normal file
View File

@ -0,0 +1,5 @@
sigs:
- name: Foo
- name: Bar
workinggroups:
- name: Baz

View File

@ -10,7 +10,7 @@
Meeting notes and Agenda can be found [here]({{.MeetingArchiveURL}}).
## Organizers
{{- range .Organizers }}
{{- range .Leads }}
* [{{.Name}}](https://github.com/{{.GitHub}}){{if .Company}}, {{.Company}}{{end}}
{{- end }}

View File

@ -645,7 +645,7 @@ workinggroups:
dir: wg-resource-management
mission_statement: >
Designing and shepherding cross-cutting features around compute resource isolation and utilization.
organizers:
leads:
- name: Vishnu Kannan
github: vishh
company: Google