Skip to content

Commit 8b59415

Browse files
committed
hivesim: Add docs generation
1 parent 4427675 commit 8b59415

File tree

8 files changed

+637
-52
lines changed

8 files changed

+637
-52
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/evanw/esbuild v0.18.11
1010
github.com/fsouza/go-dockerclient v1.9.8
1111
github.com/gorilla/mux v1.8.0
12+
github.com/lithammer/dedent v1.1.0
1213
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
1314
golang.org/x/net v0.17.0
1415
gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
226226
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
227227
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
228228
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
229+
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
230+
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
229231
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
230232
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
231233
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=

hivesim/docs.go

+375
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
package hivesim
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/ethereum/hive/internal/simapi"
11+
"github.com/lithammer/dedent"
12+
"gopkg.in/inconshreveable/log15.v2"
13+
)
14+
15+
func toMarkdownLink(title string) string {
16+
removeChars := []string{":", "#", "'", "\"", "`", "*", "+", ",", ";"}
17+
title = strings.ReplaceAll(strings.ToLower(title), " ", "-")
18+
for _, invalidChar := range removeChars {
19+
title = strings.ReplaceAll(title, invalidChar, "")
20+
}
21+
return title
22+
}
23+
24+
func formatDescription(desc string) string {
25+
desc = dedent.Dedent(desc)
26+
// Replace single quotes with backticks, since backticks are used for markdown code blocks and
27+
// they cannot be escaped in golang (when used within backticks string).
28+
desc = strings.ReplaceAll(desc, "'", "`")
29+
return desc
30+
}
31+
32+
type markdownTestCase simapi.TestRequest
33+
34+
func (tc *markdownTestCase) commandLine(simName string, suiteName string) string {
35+
return fmt.Sprintf("./hive --client <CLIENTS> --sim %s --sim.limit \"%s/%s\"", simName, suiteName, tc.Name)
36+
}
37+
38+
func (tc *markdownTestCase) toBePrinted() bool {
39+
return tc.Description != ""
40+
}
41+
42+
func (tc *markdownTestCase) displayName() string {
43+
if tc.DisplayName != "" {
44+
return tc.DisplayName
45+
}
46+
return tc.Name
47+
}
48+
49+
func (tc *markdownTestCase) description() string {
50+
return formatDescription(tc.Description)
51+
}
52+
53+
func (tc *markdownTestCase) toMarkdown(simName string, suiteName string, depth int) string {
54+
sb := strings.Builder{}
55+
56+
// Print test case display name
57+
sb.WriteString(fmt.Sprintf("%s %s\n\n", strings.Repeat("#", depth), tc.displayName()))
58+
59+
// Print command-line to run
60+
sb.WriteString(fmt.Sprintf("%s Run\n\n", strings.Repeat("#", depth+1)))
61+
sb.WriteString("<details>\n")
62+
sb.WriteString("<summary>Command-line</summary>\n\n")
63+
sb.WriteString(fmt.Sprintf("```bash\n%s\n```\n\n", tc.commandLine(simName, suiteName)))
64+
sb.WriteString("</details>\n\n")
65+
66+
// Print description
67+
sb.WriteString(fmt.Sprintf("%s Description\n\n", strings.Repeat("#", depth+1)))
68+
sb.WriteString(fmt.Sprintf("%s\n\n", tc.description()))
69+
70+
return sb.String()
71+
}
72+
73+
func getCategories(testCases map[TestID]*markdownTestCase) []string {
74+
categoryMap := make(map[string]struct{})
75+
categories := make([]string, 0)
76+
for _, tc := range testCases {
77+
if tc.toBePrinted() {
78+
if _, ok := categoryMap[tc.Category]; !ok {
79+
categories = append(categories, tc.Category)
80+
categoryMap[tc.Category] = struct{}{}
81+
}
82+
}
83+
}
84+
return categories
85+
}
86+
87+
type markdownSuite struct {
88+
simapi.TestRequest
89+
running bool
90+
tests map[TestID]*markdownTestCase
91+
}
92+
93+
func (s *markdownSuite) displayName() string {
94+
if s.DisplayName != "" {
95+
return s.DisplayName
96+
}
97+
return fmt.Sprintf("`%s`", s.Name)
98+
}
99+
100+
func (s *markdownSuite) mardownFilePath() string {
101+
if s.Location != "" {
102+
return fmt.Sprintf("%s/TESTS.md", s.Location)
103+
}
104+
title := strings.ReplaceAll(strings.ToUpper(s.Name), " ", "-")
105+
return fmt.Sprintf("TESTS-%s.md", title)
106+
}
107+
108+
func (s *markdownSuite) commandLine(simName string) string {
109+
return fmt.Sprintf("./hive --client <CLIENTS> --sim %s --sim.limit \"%s/\"", simName, s.Name)
110+
}
111+
112+
func (s *markdownSuite) description() string {
113+
return formatDescription(s.Description)
114+
}
115+
116+
func (s *markdownSuite) toMarkdown(simName string) (string, error) {
117+
headerBuilder := strings.Builder{}
118+
categoryBuilder := map[string]*strings.Builder{}
119+
headerBuilder.WriteString(fmt.Sprintf("# %s - Test Cases\n\n", s.displayName()))
120+
121+
headerBuilder.WriteString(fmt.Sprintf("%s\n\n", s.description()))
122+
123+
headerBuilder.WriteString("## Run Suite\n\n")
124+
125+
headerBuilder.WriteString("<details>\n")
126+
headerBuilder.WriteString("<summary>Command-line</summary>\n\n")
127+
headerBuilder.WriteString(fmt.Sprintf("```bash\n%s\n```\n\n", s.commandLine(simName)))
128+
headerBuilder.WriteString("</details>\n\n")
129+
130+
categories := getCategories(s.tests)
131+
132+
tcDepth := 3
133+
if len(categories) > 1 {
134+
headerBuilder.WriteString("## Test Case Categories\n\n")
135+
for _, category := range categories {
136+
if category == "" {
137+
category = "Other"
138+
}
139+
headerBuilder.WriteString(fmt.Sprintf("- [%s](#category-%s)\n\n", category, toMarkdownLink(category)))
140+
}
141+
} else {
142+
headerBuilder.WriteString("## Test Cases\n\n")
143+
}
144+
145+
for _, tc := range s.tests {
146+
if !tc.toBePrinted() {
147+
continue
148+
}
149+
contentBuilder, ok := categoryBuilder[tc.Category]
150+
if !ok {
151+
contentBuilder = &strings.Builder{}
152+
categoryBuilder[tc.Category] = contentBuilder
153+
}
154+
contentBuilder.WriteString(tc.toMarkdown(simName, s.Name, tcDepth))
155+
}
156+
157+
if len(categoryBuilder) > 1 {
158+
for _, category := range categories {
159+
if category == "" {
160+
category = "Other"
161+
}
162+
contentBuilder, ok := categoryBuilder[category]
163+
if !ok {
164+
continue
165+
}
166+
headerBuilder.WriteString(fmt.Sprintf("## Category: %s\n\n", category))
167+
headerBuilder.WriteString(contentBuilder.String())
168+
}
169+
} else {
170+
for _, contentBuilder := range categoryBuilder {
171+
headerBuilder.WriteString(contentBuilder.String())
172+
}
173+
}
174+
175+
return headerBuilder.String(), nil
176+
}
177+
178+
func (s *markdownSuite) toMarkdownFile(fw FileWriter, simName string) error {
179+
// Create the file.
180+
file, err := fw.CreateWriter(s.mardownFilePath())
181+
if err != nil {
182+
return err
183+
}
184+
defer file.Close()
185+
186+
// Write the markdown.
187+
markdown, err := s.toMarkdown(simName)
188+
if err != nil {
189+
return err
190+
}
191+
_, err = file.Write([]byte(markdown))
192+
return err
193+
}
194+
195+
// Docs collector object
196+
type docsCollector struct {
197+
simName string
198+
outputDir string
199+
suites map[SuiteID]*markdownSuite
200+
}
201+
202+
func simulatorNameFromBinaryPath() string {
203+
execPath, err := os.Executable()
204+
if err != nil {
205+
panic(err)
206+
}
207+
var (
208+
path = filepath.Dir(execPath)
209+
name = filepath.Base(path)
210+
names = make([]string, 0)
211+
)
212+
for path != "/" && name != "simulators" {
213+
names = append([]string{name}, names...)
214+
path = filepath.Dir(path)
215+
name = filepath.Base(path)
216+
}
217+
return strings.Join(names, "/")
218+
}
219+
220+
// NewDocsCollector creates a new docs collector object.
221+
func NewDocsCollector() *docsCollector {
222+
docs := &docsCollector{
223+
suites: make(map[SuiteID]*markdownSuite),
224+
}
225+
// Set the simulation name: if `HIVE_SIMULATOR_NAME` is set, use that, otherwise use the
226+
// folder name and its parent.
227+
docs.simName = os.Getenv("HIVE_SIMULATOR_NAME")
228+
if docs.simName == "" {
229+
docs.simName = simulatorNameFromBinaryPath()
230+
}
231+
232+
// Set the output directory: if `HIVE_DOCS_OUTPUT_DIR` is set, use that, otherwise use the
233+
// current directory.
234+
docs.outputDir = os.Getenv("HIVE_DOCS_OUTPUT_DIR")
235+
if docs.outputDir == "" {
236+
var err error
237+
docs.outputDir, err = os.Getwd()
238+
if err != nil {
239+
panic(err)
240+
}
241+
}
242+
243+
return docs
244+
}
245+
246+
// Returns true if any suite is still running
247+
func (docs *docsCollector) AnyRunning() bool {
248+
for _, s := range docs.suites {
249+
if s.running {
250+
return true
251+
}
252+
}
253+
return false
254+
}
255+
256+
func (docs *docsCollector) StartSuite(suite *simapi.TestRequest, simlog string) (SuiteID, error) {
257+
// Create a new markdown suite.
258+
markdownSuite := &markdownSuite{
259+
TestRequest: *suite,
260+
running: true,
261+
tests: make(map[TestID]*markdownTestCase),
262+
}
263+
// Next suite id
264+
suiteID := SuiteID(len(docs.suites))
265+
// Add the suite to the map.
266+
docs.suites[suiteID] = markdownSuite
267+
// Return the suite ID.
268+
return suiteID, nil
269+
}
270+
271+
func (docs *docsCollector) EndSuite(testSuite SuiteID) error {
272+
suite, ok := docs.suites[testSuite]
273+
if !ok {
274+
return fmt.Errorf("test suite %d does not exist", testSuite)
275+
}
276+
suite.running = false
277+
if !docs.AnyRunning() {
278+
// Generate markdown files when all suites are done.
279+
if err := docs.genSimulatorMarkdownFiles(NewFileWriter(docs.outputDir)); err != nil {
280+
log15.Error("can't generate markdown files", "err", err)
281+
}
282+
}
283+
return nil
284+
}
285+
286+
func (docs *docsCollector) StartTest(testSuite SuiteID, test *simapi.TestRequest) (TestID, error) {
287+
// Create a new markdown test case.
288+
markdownTest := markdownTestCase(*test)
289+
// Check if suite exists.
290+
if _, ok := docs.suites[testSuite]; !ok {
291+
return 0, fmt.Errorf("test suite %d does not exist", testSuite)
292+
}
293+
// Next test id
294+
testID := TestID(len(docs.suites[testSuite].tests))
295+
// Add the test to the map.
296+
docs.suites[testSuite].tests[testID] = &markdownTest
297+
// Return the test ID.
298+
return testID, nil
299+
}
300+
301+
func (docs *docsCollector) EndTest(testSuite SuiteID, test TestID, testResult TestResult) error {
302+
// No-op in docs mode.
303+
return nil
304+
}
305+
306+
func (docs *docsCollector) ClientTypes() ([]*ClientDefinition, error) {
307+
// Return a dummy "docs" client type.
308+
return []*ClientDefinition{
309+
{
310+
Name: "Client",
311+
Version: "1.0.0",
312+
},
313+
}, nil
314+
}
315+
316+
func (docs *docsCollector) generateIndex(simName string) (string, error) {
317+
headerBuilder := strings.Builder{}
318+
headerBuilder.WriteString(fmt.Sprintf("# Simulator `%s` Test Cases\n\n", simName))
319+
headerBuilder.WriteString("## Test Suites\n\n")
320+
for _, s := range docs.suites {
321+
headerBuilder.WriteString(fmt.Sprintf("### - [%s](%s)\n", s.displayName(), s.mardownFilePath()))
322+
headerBuilder.WriteString(s.Description)
323+
headerBuilder.WriteString("\n\n")
324+
}
325+
return headerBuilder.String() + "\n", nil
326+
}
327+
328+
func (docs *docsCollector) generateIndexFile(fw FileWriter, simName string) error {
329+
markdownIndex, err := docs.generateIndex(simName)
330+
if err != nil {
331+
return err
332+
}
333+
file, err := fw.CreateWriter("TESTS.md")
334+
if err != nil {
335+
return err
336+
}
337+
defer file.Close()
338+
_, err = file.Write([]byte(markdownIndex))
339+
return err
340+
}
341+
342+
type FileWriter interface {
343+
CreateWriter(path string) (io.WriteCloser, error)
344+
}
345+
346+
type fileWriter struct {
347+
path string
348+
}
349+
350+
var _ FileWriter = (*fileWriter)(nil)
351+
352+
func (fw *fileWriter) CreateWriter(path string) (io.WriteCloser, error) {
353+
filePath := filepath.FromSlash(fmt.Sprintf("%s/%s", fw.path, path))
354+
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
355+
return nil, err
356+
}
357+
return os.Create(filePath)
358+
}
359+
360+
func NewFileWriter(path string) FileWriter {
361+
return &fileWriter{path}
362+
}
363+
364+
func (docs *docsCollector) genSimulatorMarkdownFiles(fw FileWriter) error {
365+
err := docs.generateIndexFile(fw, docs.simName)
366+
if err != nil {
367+
return err
368+
}
369+
for _, s := range docs.suites {
370+
if err := s.toMarkdownFile(fw, docs.simName); err != nil {
371+
return err
372+
}
373+
}
374+
return nil
375+
}

0 commit comments

Comments
 (0)