This commit is contained in:
lealife
2017-06-22 13:18:16 +08:00
parent 2654b684df
commit b140cd538f
549 changed files with 185885 additions and 1 deletions

View File

@@ -0,0 +1,327 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package controllers
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"reflect"
"sort"
"strings"
"github.com/revel/revel"
"github.com/revel/revel/testing"
)
// TestRunner is a controller which is used for running application tests in browser.
type TestRunner struct {
*revel.Controller
}
// TestSuiteDesc is used for storing information about a single test suite.
// This structure is required by revel test cmd.
type TestSuiteDesc struct {
Name string
Tests []TestDesc
// Elem is reflect.Type which can be used for accessing methods
// of the test suite.
Elem reflect.Type
}
// TestDesc is used for describing a single test of some test suite.
// This structure is required by revel test cmd.
type TestDesc struct {
Name string
}
// TestSuiteResult stores the results the whole test suite.
// This structure is required by revel test cmd.
type TestSuiteResult struct {
Name string
Passed bool
Results []TestResult
}
// TestResult represents the results of running a single test of some test suite.
// This structure is required by revel test cmd.
type TestResult struct {
Name string
Passed bool
ErrorHTML template.HTML
ErrorSummary string
}
var (
testSuites []TestSuiteDesc // A list of all available tests.
none = []reflect.Value{} // It is used as input for reflect call in a few places.
// registeredTests simplifies the search of test suites by their name.
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
registeredTests map[string]int
)
/*
Controller's action methods are below.
*/
// Index is an action which renders the full list of available test suites and their tests.
func (c TestRunner) Index() revel.Result {
c.ViewArgs["suiteFound"] = len(testSuites) > 0
return c.Render(testSuites)
}
// Suite method allows user to navigate to individual Test Suite and their tests
func (c TestRunner) Suite(suite string) revel.Result {
var foundTestSuites []TestSuiteDesc
for _, testSuite := range testSuites {
if strings.EqualFold(testSuite.Name, suite) {
foundTestSuites = append(foundTestSuites, testSuite)
}
}
c.ViewArgs["testSuites"] = foundTestSuites
c.ViewArgs["suiteFound"] = len(foundTestSuites) > 0
c.ViewArgs["suiteName"] = suite
return c.RenderTemplate("TestRunner/Index.html")
}
// Run runs a single test, given by the argument.
func (c TestRunner) Run(suite, test string) revel.Result {
// Check whether requested test exists.
suiteIndex, ok := registeredTests[suite+"."+test]
if !ok {
return c.NotFound("Test %s.%s does not exist", suite, test)
}
result := TestResult{Name: test}
// Found the suite, create a new instance and run the named method.
t := testSuites[suiteIndex].Elem
v := reflect.New(t)
func() {
// When the function stops executing try to recover from panic.
defer func() {
if err := recover(); err != nil {
// If panic error is empty, exit.
panicErr := revel.NewErrorFromPanic(err)
if panicErr == nil {
return
}
// Otherwise, prepare and format the response of server if possible.
testSuite := v.Elem().FieldByName("TestSuite").Interface().(testing.TestSuite)
res := formatResponse(testSuite)
// Render the error and save to the result structure.
var buffer bytes.Buffer
tmpl, _ := revel.MainTemplateLoader.TemplateLang("TestRunner/FailureDetail.html", "")
_ = tmpl.Render(&buffer, map[string]interface{}{
"error": panicErr,
"response": res,
"postfix": suite + "_" + test,
})
result.ErrorSummary = errorSummary(panicErr)
result.ErrorHTML = template.HTML(buffer.String())
}
}()
// Initialize the test suite with a NewTestSuite()
testSuiteInstance := v.Elem().FieldByName("TestSuite")
testSuiteInstance.Set(reflect.ValueOf(testing.NewTestSuite()))
// Make sure After method will be executed at the end.
if m := v.MethodByName("After"); m.IsValid() {
defer m.Call(none)
}
// Start from running Before method of test suite if exists.
if m := v.MethodByName("Before"); m.IsValid() {
m.Call(none)
}
// Start the test method itself.
v.MethodByName(test).Call(none)
// No panic means success.
result.Passed = true
}()
return c.RenderJSON(result)
}
// List returns a JSON list of test suites and tests.
// It is used by revel test command line tool.
func (c TestRunner) List() revel.Result {
return c.RenderJSON(testSuites)
}
/*
Below are helper functions.
*/
// describeSuite expects testsuite interface as input parameter
// and returns its description in a form of TestSuiteDesc structure.
func describeSuite(testSuite interface{}) TestSuiteDesc {
t := reflect.TypeOf(testSuite)
// Get a list of methods of the embedded test type.
// It will be used to make sure the same tests are not included in multiple test suites.
super := t.Elem().Field(0).Type
superMethods := map[string]bool{}
for i := 0; i < super.NumMethod(); i++ {
// Save the current method's name.
superMethods[super.Method(i).Name] = true
}
// Get a list of methods on the test suite that take no parameters, return
// no results, and were not part of the embedded type's method set.
var tests []TestDesc
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
mt := m.Type
// Make sure the test method meets the criterias:
// - method of testSuite without input parameters;
// - nothing is returned;
// - has "Test" prefix;
// - doesn't belong to the embedded structure.
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
nothingReturned := (mt.NumOut() == 0)
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
// Register the test suite's index so we can quickly find it by test's name later.
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
// Add test to the list of tests.
tests = append(tests, TestDesc{m.Name})
}
}
return TestSuiteDesc{
Name: t.Elem().Name(),
Tests: tests,
Elem: t.Elem(),
}
}
// errorSummary gets an error and returns its summary in human readable format.
func errorSummary(err *revel.Error) (message string) {
expectedPrefix := "(expected)"
actualPrefix := "(actual)"
errDesc := err.Description
//strip the actual/expected stuff to provide more condensed display.
if strings.Index(errDesc, expectedPrefix) == 0 {
errDesc = errDesc[len(expectedPrefix):]
}
if strings.LastIndex(errDesc, actualPrefix) > 0 {
errDesc = errDesc[0 : len(errDesc)-len(actualPrefix)]
}
errFile := err.Path
slashIdx := strings.LastIndex(errFile, "/")
if slashIdx > 0 {
errFile = errFile[slashIdx+1:]
}
message = fmt.Sprintf("%s %s#%d", errDesc, errFile, err.Line)
/*
// If line of error isn't known return the message as is.
if err.Line == 0 {
return
}
// Otherwise, include info about the line number and the relevant
// source code lines.
message += fmt.Sprintf(" (around line %d): ", err.Line)
for _, line := range err.ContextSource() {
if line.IsError {
message += line.Source
}
}
*/
return
}
// formatResponse gets *revel.TestSuite as input parameter and
// transform response related info into a readable format.
func formatResponse(t testing.TestSuite) map[string]string {
if t.Response == nil {
return map[string]string{}
}
// Since Go 1.6 http.Request struct contains `Cancel <-chan struct{}` which
// results in `json: unsupported type: <-chan struct {}`
// So pull out required things for Request and Response
req := map[string]interface{}{
"Method": t.Response.Request.Method,
"URL": t.Response.Request.URL,
"Proto": t.Response.Request.Proto,
"ContentLength": t.Response.Request.ContentLength,
"Header": t.Response.Request.Header,
"Form": t.Response.Request.Form,
"PostForm": t.Response.Request.PostForm,
}
resp := map[string]interface{}{
"Status": t.Response.Status,
"StatusCode": t.Response.StatusCode,
"Proto": t.Response.Proto,
"Header": t.Response.Header,
"ContentLength": t.Response.ContentLength,
"TransferEncoding": t.Response.TransferEncoding,
}
// Beautify the response JSON to make it human readable.
respBytes, err := json.MarshalIndent(
map[string]interface{}{
"Response": resp,
"Request": req,
},
"",
" ")
if err != nil {
fmt.Println(err)
}
// Remove extra new line symbols so they do not take too much space on a result page.
// Allow no more than 1 line break at a time.
body := strings.Replace(string(t.ResponseBody), "\n\n", "\n", -1)
body = strings.Replace(body, "\r\n\r\n", "\r\n", -1)
return map[string]string{
"Headers": string(respBytes),
"Body": strings.TrimSpace(body),
}
}
//sortbySuiteName sorts the testsuites by name.
type sortBySuiteName []interface{}
func (a sortBySuiteName) Len() int { return len(a) }
func (a sortBySuiteName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortBySuiteName) Less(i, j int) bool {
return reflect.TypeOf(a[i]).Elem().Name() < reflect.TypeOf(a[j]).Elem().Name()
}
func init() {
// Every time app is restarted convert the list of available test suites
// provided by the revel testing package into a format which will be used by
// the testrunner module and revel test cmd.
revel.OnAppStart(func() {
// Extracting info about available test suites from revel/testing package.
registeredTests = map[string]int{}
sort.Sort(sortBySuiteName(testing.TestSuites))
for _, testSuite := range testing.TestSuites {
testSuites = append(testSuites, describeSuite(testSuite))
}
})
}

View File

@@ -0,0 +1,12 @@
package app
import (
"fmt"
"github.com/revel/revel"
)
func init() {
revel.OnAppStart(func() {
fmt.Println("Go to /@tests to run the tests.")
})
}

View File

@@ -0,0 +1,45 @@
<div class="panel panel-default">
<div class="panel-heading">
<b>{{.error.Description}}</b>
</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#error_{{.postfix}}" role="tab" data-toggle="tab">Error</a></li>
<li><a href="#stack_{{.postfix}}" role="tab" data-toggle="tab">Stack</a></li>
{{if .response}}
<li><a href="#headers_{{.postfix}}" role="tab" data-toggle="tab">Headers</a></li>
<li><a href="#body_{{.postfix}}" role="tab" data-toggle="tab">Response Body</a></li>
{{end}}
</ul>
<div class="tab-content" id="result_{{.postfix}}">
<div class="tab-pane active" id="error_{{.postfix}}">
<div class="panel panel-danger">
<div class="panel-heading">
In {{.error.Path}}{{if .error.Line}} (around {{if .error.Line}}line {{.error.Line}}{{end}}{{if .error.Column}} column {{.error.Column}}{{end}}){{end}}:
</div>
<div class="panel-body">
{{range .error.ContextSource}}
{{if .IsError}}
<pre><code class="go">{{.Source}}</code></pre>
{{end}}
{{end}}
</div>
</div>
</div>
<div class="tab-pane" id="stack_{{.postfix}}">
<pre><code class="bash">{{.error.Stack}}</code></pre>
</div>
{{if .response}}
<div class="tab-pane" id="headers_{{.postfix}}">
<pre><code class="json">{{.response.Headers}}</code></pre>
</div>
<div class="tab-pane" id="body_{{.postfix}}">
<pre><code class="html">{{.response.Body}}</code></pre>
</div>
{{end}}
</div>
</div>
</div>

View File

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html>
<head>
<title>Revel Test Runner</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="{{url `Root`}}/@tests/public/css/bootstrap.min.css" type="text/css" rel="stylesheet"></link>
<link href="{{url `Root`}}/@tests/public/css/github.css" type="text/css" rel="stylesheet"></link>
<script src="{{url `Root`}}/@tests/public/js/jquery-1.9.1.min.js" type="text/javascript"></script>
<script src="{{url `Root`}}/@tests/public/js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{url `Root`}}/@tests/public/js/highlight.pack.js" type="text/javascript"></script>
<style>
header { background-color:#ADD8E6 }
header h1 {margin-top: 10px; margin-bottom:20px;}
header table {margin-bottom: 0px }
td .btn {margin-bottom: 1px; }
button.file-test { margin-bottom: 0px; margin-left: 2px }
table.tests tr { border-bottom: 1px solid #ddd; background-color: #f9f9f9; }
.passed td { background-color: #90EE90 !important; }
.failed td { background-color: #FFB6C1 !important; }
td.result div.panel-default{ display:none; }
td.result > a { color: red; }
td.rightCol, td.leftCol { width: 40px; }
pre { font-size:10px; white-space: pre; }
.panel-heading {
padding: 10px 5px 8px 5px
}
.name { width: 35%; }
.w100 { width: 100%; }
.logo, .logo:hover { text-decoration: none; color: inherit;}
.pnt-triangle { color: #777; font-size: 18px;}
.panel-group .panel-heading+.panel-collapse>.panel-body { border-top: none;}
</style>
</head>
<body>
<header>
<div class="container">
<h1 class="pull-left">
<a href="/@tests" class="logo">Test Runner</a> <small>- Run your application's tests here.</small>
</h1>
<div style="margin-top:16px" class="pull-right">
<button class="btn btn-success {{if not .suiteFound}}disabled{{end}}" all-tests="">Run All Tests</button>
<div><a class="small" href="#" id="allTestResults"></a></div>
</div>
</div>
</header>
<div class="panel-group container">
{{if not .suiteFound}}
<div style="margin-top:20px;padding:15px;" class="panel panel-default">
<span style="font-weight:bold;color:#777;">Suite "{{.suiteName}}" is not found.</span>
</div>
{{end}}
{{range .testSuites}}
{{ $testFile := .Name }}
<div style="margin-top:20px;" class="panel panel-default">
<div class="panel-heading collapseLnk" style="cursor:pointer" data-toggle="collapse" data-target="#{{.Name}}">
<button id="suite{{.Name}}" class="btn btn-xs btn-success" test-file="{{.Name}}">Run</button>
<span class="h5">&nbsp;<a href="/@tests/{{ .Name }}">{{.Name}}</a></span>
<span id="pointTriangle{{.Name}}" class="pnt-triangle pull-right" data-tri-open="open">&#9660</span>
</div>
<div id="{{.Name}}" class="panel-collapse collapse in">
<table class="panel-body table table-condensed tests" suite="{{.Name}}">
{{range .Tests}}
<tr id="testRow{{.Name}}">
<td class="leftCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="leftbutton btn btn-success btn-xs">Run</button></td>
<td class="name">{{ .Name }}</td>
<td class="result"><a href="#"></a></td>
<td class="rightCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="pull-right btn btn-success btn-xs">Run</button></td>
</tr>
{{end}}
</table>
</div>
</div>
{{end}}
</div>
<script>
var passCount = 0;
var failCount = 0;
var buttons = [];
var running;
$(function() {
var divId, visible;
var oneOpened = false;
var panels = $("div.panel-collapse");
//close any panels that were previously closed.
panels.each(function() {
divId = "#" + $(this).attr("id");
visible = parseBoolean(localStorage.getItem("testrunner_" + divId));
if (visible) {
togglePntTriangle(divId, visible);
oneOpened = visible;
} else {
$(divId).css("height", "0").removeClass("in"); //this is the way bootstrap does it.
$("div[data-target=" + divId + "]").addClass("collapsed");
togglePntTriangle(divId, visible);
}
});
if (!oneOpened && panels.length > 0) {
divId = "#" + panels[0].id;
$(divId).collapse('show');
togglePntTriangle(divId, true);
}
});
$(".fileTestLnk").click(function() {
var tableId = $(this).attr("href");
$(tableId).toggle();
return false;
});
$("#allTestResults").click(function() {
var badRows = $("tr.failed");
if (badRows.length >= 0) {
badRows[0].scrollIntoView();
}
return false;
});
$("button[test]").click(function() {
$("#allTestResults").text("");
var button = $(this).addClass("disabled").text("Running");
$(this).closest("tr").removeClass("passed").removeClass("failed");
addToQueue(button);
});
$("td.result a").click(function() { //show/hide the extended error div
$(this).siblings().toggle();
return false;
});
$("button[test-file]").click(function() {
$("#allTestResults").text("");
var testfile = $(this).attr('test-file');
$("button").each(function() {
if ($(this).data("test-file") == testfile)
$(this).click();
});
return false;
});
$("button[all-tests]").click(function() {
$("tr").removeClass("passed").removeClass("failed");
passCount = 0;
failCount = 0;
var button = $(this).addClass("disabled").text("Running");
$("button.leftbutton[test]").click();
});
$("div.collapseLnk").click(function() {
var tableId = $(this).data("target");
var visible = !$(tableId).is(":visible");
localStorage.setItem("testrunner_" + tableId, visible);
togglePntTriangle(tableId, visible);
});
function togglePntTriangle(suiteId, visible) {
if (suiteId.charAt(0) == '#') {
suiteId = suiteId.substring(1);
}
$('#pointTriangle' + suiteId).html(visible ? '&#9660' : '&#9654');
}
function parseBoolean(str) {
return /^true$/i.test(str);
}
function addToQueue(button) {
buttons.push(button);
if (!running) {
running = true;
nextTest();
}
}
function nextTest() {
if (buttons.length == 0) {
running = false;
} else {
var next = buttons.shift();
runTest(next);
}
}
function runTest(button) {
var suite = button.parents("table").attr("suite");
var test = button.attr("test");
var row = button.parents("tr");
var resultCell = row.children(".result");
$("a", resultCell).text("");
$("div.panel-default", resultCell).remove();
$.ajax({
dataType: "json",
url: "{{url `Root`}}/@tests/"+suite+"/"+test,
success: function(result) {
row.attr("class", result.Passed ? "passed" : "failed");
if (result.Passed) {
passCount++;
} else {
console.log("fail:", result.Name);
failCount++;
var resultLnk = $("a", resultCell);
$(resultLnk).text(result.ErrorSummary);
resultCell.append(result.ErrorHTML);
var pnlDiv = row.closest("div");
if ($(pnlDiv).hasClass("in") == false) {
$("#link-" + suite).click();
}
$("#result_" + suite + "_" + test + " pre code").each(function(i, block) {
hljs.highlightBlock(block);
});
}
button.removeClass("disabled").text("Run");
var runAllBut = $("button[all-tests]");
if (buttons.length == 0 && runAllBut.hasClass("disabled")) {
runAllBut.removeClass("disabled").text("Run All Tests");
var resMsg = passCount + " passed, " + failCount + " failed.";
$("#allTestResults").text(resMsg);
}
nextTest();
}
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<title>Revel Test Runner</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
header { padding:20px 0; }
header.passed { background-color: #90EE90 !important; }
header.failed { background-color: #FFB6C1 !important; }
table { margin-top: 20px; padding: 8px; line-height: 20px; }
td { vertical-align: top; padding-right:20px; }
a { color: #0088cc; }
.container { margin-left: auto; margin-right: auto; width: 940px; overflow: hidden; }
.result h2 { font-size: 16px; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.2em; }
.result.failed b { font-weight:bold; color: #C00; font-size: 14px; }
.result.failed h2 { color: #C00; }
.result .info { font-size: 12px; }
.result .info pre { overflow: auto; background-color: #f0f0f0; width: 100%; max-height: 500px; }
</style>
</head>
<body>
<header class="{{if .Passed}}passed{{else}}failed{{end}}">
<div class="container">
<h1>{{.Name}}</h1>
<p>{{if .Passed}}PASSED{{else}}FAILED{{end}}</p>
</div>
</header>
<div class="container">
{{range .Results}}
<div class="result {{if .Passed}}passed{{else}}failed{{end}}">
<div><h2>{{.Name}}</h2></div>
<div class="info">{{if .ErrorHTML}}{{.ErrorHTML}}{{else}}PASSED{{end}}</div>
</div>
{{end}}
</div>
</body>
</html>