fix: import path should be github.com/ooni/probe-cli/v3 (#200)
See https://github.com/ooni/probe/issues/1335#issuecomment-771499511
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/fatih/color"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/utils"
|
||||
)
|
||||
|
||||
// Default handler outputting to stderr.
|
||||
var Default = New(os.Stdout)
|
||||
|
||||
// start time.
|
||||
var start = time.Now()
|
||||
|
||||
var bold = color.New(color.Bold)
|
||||
|
||||
// Colors mapping.
|
||||
var Colors = [...]*color.Color{
|
||||
log.DebugLevel: color.New(color.FgWhite),
|
||||
log.InfoLevel: color.New(color.FgBlue),
|
||||
log.WarnLevel: color.New(color.FgYellow),
|
||||
log.ErrorLevel: color.New(color.FgRed),
|
||||
log.FatalLevel: color.New(color.FgRed),
|
||||
}
|
||||
|
||||
// Strings mapping.
|
||||
var Strings = [...]string{
|
||||
log.DebugLevel: "•",
|
||||
log.InfoLevel: "•",
|
||||
log.WarnLevel: "•",
|
||||
log.ErrorLevel: "⨯",
|
||||
log.FatalLevel: "⨯",
|
||||
}
|
||||
|
||||
// Handler implementation.
|
||||
type Handler struct {
|
||||
mu sync.Mutex
|
||||
Writer io.Writer
|
||||
Padding int
|
||||
}
|
||||
|
||||
// New handler.
|
||||
func New(w io.Writer) *Handler {
|
||||
if f, ok := w.(*os.File); ok {
|
||||
return &Handler{
|
||||
Writer: colorable.NewColorable(f),
|
||||
Padding: 3,
|
||||
}
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
Writer: w,
|
||||
Padding: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func logSectionTitle(w io.Writer, f log.Fields) error {
|
||||
colWidth := 24
|
||||
|
||||
title := f.Get("title").(string)
|
||||
fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth+2)+"┓\n")
|
||||
fmt.Fprintf(w, "┃ %s ┃\n", utils.RightPad(title, colWidth))
|
||||
fmt.Fprintf(w, "┗"+strings.Repeat("━", colWidth+2)+"┛\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func logTable(w io.Writer, f log.Fields) error {
|
||||
color := color.New(color.FgBlue)
|
||||
|
||||
names := f.Names()
|
||||
|
||||
var lines []string
|
||||
colWidth := 0
|
||||
for _, name := range names {
|
||||
if name == "type" {
|
||||
continue
|
||||
}
|
||||
line := fmt.Sprintf("%s: %s", color.Sprint(name), f.Get(name))
|
||||
lineLength := utils.EscapeAwareRuneCountInString(line)
|
||||
lines = append(lines, line)
|
||||
if colWidth < lineLength {
|
||||
colWidth = lineLength
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth+2)+"┓\n")
|
||||
for _, line := range lines {
|
||||
fmt.Fprintf(w, "┃ %s ┃\n",
|
||||
utils.RightPad(line, colWidth),
|
||||
)
|
||||
}
|
||||
fmt.Fprintf(w, "┗"+strings.Repeat("━", colWidth+2)+"┛\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TypedLog is used for handling special "typed" logs to the CLI
|
||||
func (h *Handler) TypedLog(t string, e *log.Entry) error {
|
||||
switch t {
|
||||
case "engine":
|
||||
fmt.Fprintf(h.Writer, "[engine] %s\n", e.Message)
|
||||
return nil
|
||||
case "progress":
|
||||
perc := e.Fields.Get("percentage").(float64) * 100
|
||||
eta := e.Fields.Get("eta").(float64)
|
||||
var etaMessage string
|
||||
if eta >= 0 {
|
||||
etaMessage = fmt.Sprintf("(%ss left)", bold.Sprintf("%.2f", eta))
|
||||
}
|
||||
s := fmt.Sprintf(" %s %-25s %s",
|
||||
bold.Sprintf("%.2f%%", perc),
|
||||
e.Message, etaMessage)
|
||||
fmt.Fprint(h.Writer, s)
|
||||
fmt.Fprintln(h.Writer)
|
||||
return nil
|
||||
case "table":
|
||||
return logTable(h.Writer, e.Fields)
|
||||
case "measurement_item":
|
||||
return logMeasurementItem(h.Writer, e.Fields)
|
||||
case "measurement_json":
|
||||
return logMeasurementJSON(h.Writer, e.Fields)
|
||||
case "measurement_summary":
|
||||
return logMeasurementSummary(h.Writer, e.Fields)
|
||||
case "result_item":
|
||||
return logResultItem(h.Writer, e.Fields)
|
||||
case "result_summary":
|
||||
return logResultSummary(h.Writer, e.Fields)
|
||||
case "section_title":
|
||||
return logSectionTitle(h.Writer, e.Fields)
|
||||
default:
|
||||
return h.DefaultLog(e)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLog is the default way of printing out logs
|
||||
func (h *Handler) DefaultLog(e *log.Entry) error {
|
||||
color := Colors[e.Level]
|
||||
level := Strings[e.Level]
|
||||
names := e.Fields.Names()
|
||||
|
||||
s := color.Sprintf("%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message)
|
||||
for _, name := range names {
|
||||
if name == "source" {
|
||||
continue
|
||||
}
|
||||
s += fmt.Sprintf(" %s=%v", color.Sprint(name), e.Fields.Get(name))
|
||||
}
|
||||
|
||||
fmt.Fprint(h.Writer, s)
|
||||
fmt.Fprintln(h.Writer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleLog implements log.Handler.
|
||||
func (h *Handler) HandleLog(e *log.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
t, isTyped := e.Fields["type"].(string)
|
||||
if isTyped {
|
||||
return h.TypedLog(t, e)
|
||||
}
|
||||
|
||||
return h.DefaultLog(e)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/utils"
|
||||
)
|
||||
|
||||
func statusIcon(ok bool) string {
|
||||
if ok {
|
||||
return "✓"
|
||||
}
|
||||
return "❌"
|
||||
}
|
||||
|
||||
func logTestKeys(w io.Writer, testKeys string) error {
|
||||
colWidth := 24
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := json.Indent(&out, []byte(testKeys), "", " "); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testKeysLines := strings.Split(string(out.Bytes()), "\n")
|
||||
if len(testKeysLines) > 1 {
|
||||
testKeysLines = testKeysLines[1 : len(testKeysLines)-1]
|
||||
testKeysLines[0] = "{" + testKeysLines[0][1:]
|
||||
testKeysLines[len(testKeysLines)-1] = testKeysLines[len(testKeysLines)-1] + "}"
|
||||
}
|
||||
for _, line := range testKeysLines {
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s │\n",
|
||||
utils.RightPad(line, colWidth*2)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logMeasurementItem(w io.Writer, f log.Fields) error {
|
||||
colWidth := 24
|
||||
|
||||
rID := f.Get("id").(int64)
|
||||
testName := f.Get("test_name").(string)
|
||||
|
||||
// We currently don't use these fields in the view
|
||||
//testGroupName := f.Get("test_group_name").(string)
|
||||
//networkName := f.Get("network_name").(string)
|
||||
//asn := fmt.Sprintf("AS%d (%s)", f.Get("asn").(uint), f.Get("network_country_code").(string))
|
||||
testKeys := f.Get("test_keys").(string)
|
||||
|
||||
isAnomaly := f.Get("is_anomaly").(bool)
|
||||
isFailed := f.Get("is_failed").(bool)
|
||||
isUploaded := f.Get("is_uploaded").(bool)
|
||||
url := f.Get("url").(string)
|
||||
urlCategoryCode := f.Get("url_category_code").(string)
|
||||
|
||||
isFirst := f.Get("is_first").(bool)
|
||||
isLast := f.Get("is_last").(bool)
|
||||
if isFirst {
|
||||
fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth*2+2)+"┓\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "┢"+strings.Repeat("━", colWidth*2+2)+"┪\n")
|
||||
}
|
||||
|
||||
anomalyStr := fmt.Sprintf("ok: %s", statusIcon(!isAnomaly))
|
||||
uploadStr := fmt.Sprintf("uploaded: %s", statusIcon(isUploaded))
|
||||
failureStr := fmt.Sprintf("success: %s", statusIcon(!isFailed))
|
||||
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s │\n",
|
||||
utils.RightPad(
|
||||
fmt.Sprintf("#%d", rID), colWidth*2)))
|
||||
|
||||
if url != "" {
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s │\n",
|
||||
utils.RightPad(
|
||||
fmt.Sprintf("%s (%s)", url, urlCategoryCode), colWidth*2)))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n",
|
||||
utils.RightPad(testName, colWidth),
|
||||
utils.RightPad(anomalyStr, colWidth)))
|
||||
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n",
|
||||
utils.RightPad(failureStr, colWidth),
|
||||
utils.RightPad(uploadStr, colWidth)))
|
||||
|
||||
if testKeys != "" {
|
||||
if err := logTestKeys(w, testKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isLast {
|
||||
fmt.Fprintf(w, "└┬────────────────────────────────────────────────┬┘\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logMeasurementSummary(w io.Writer, f log.Fields) error {
|
||||
colWidth := 12
|
||||
|
||||
totalCount := f.Get("total_count").(int64)
|
||||
anomalyCount := f.Get("anomaly_count").(int64)
|
||||
totalRuntime := f.Get("total_runtime").(float64)
|
||||
dataUp := f.Get("data_usage_up").(float64)
|
||||
dataDown := f.Get("data_usage_down").(float64)
|
||||
|
||||
startTime := f.Get("start_time").(time.Time)
|
||||
|
||||
asn := f.Get("asn").(uint)
|
||||
countryCode := f.Get("network_country_code").(string)
|
||||
networkName := f.Get("network_name").(string)
|
||||
|
||||
fmt.Fprintf(w, " │ %s │\n",
|
||||
utils.RightPad(startTime.Format(time.RFC822), (colWidth+3)*3),
|
||||
)
|
||||
fmt.Fprintf(w, " │ %s │\n",
|
||||
utils.RightPad(fmt.Sprintf("AS%d, %s (%s)", asn, networkName, countryCode), (colWidth+3)*3),
|
||||
)
|
||||
fmt.Fprintf(w, " │ %s %s %s │\n",
|
||||
utils.RightPad(fmt.Sprintf("%.2fs", totalRuntime), colWidth),
|
||||
utils.RightPad(fmt.Sprintf("%d/%d anmls", anomalyCount, totalCount), colWidth),
|
||||
utils.RightPad(fmt.Sprintf("⬆ %s ⬇ %s", formatSize(dataUp), formatSize(dataDown)), colWidth+4))
|
||||
fmt.Fprintf(w, " └────────────────────────────────────────────────┘\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func logMeasurementJSON(w io.Writer, f log.Fields) error {
|
||||
m := f.Get("measurement_json").(map[string]interface{})
|
||||
|
||||
json, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, string(json))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Package progress provides a simple terminal progress bar.
|
||||
package progress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Bar is a progress bar.
|
||||
type Bar struct {
|
||||
StartDelimiter string // StartDelimiter for the bar ("|").
|
||||
EndDelimiter string // EndDelimiter for the bar ("|").
|
||||
Filled string // Filled section representation ("█").
|
||||
Empty string // Empty section representation ("░")
|
||||
Total float64 // Total value.
|
||||
Width int // Width of the bar.
|
||||
|
||||
value float64
|
||||
tmpl *template.Template
|
||||
text string
|
||||
}
|
||||
|
||||
// New returns a new bar with the given total.
|
||||
func New(total float64) *Bar {
|
||||
b := &Bar{
|
||||
StartDelimiter: "|",
|
||||
EndDelimiter: "|",
|
||||
Filled: "█",
|
||||
Empty: "░",
|
||||
Total: total,
|
||||
Width: 60,
|
||||
}
|
||||
|
||||
b.Template(`{{.Percent | printf "%3.0f"}}% {{.Bar}} {{.Text}}`)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// NewInt returns a new bar with the given total.
|
||||
func NewInt(total int) *Bar {
|
||||
return New(float64(total))
|
||||
}
|
||||
|
||||
// Text sets the text value.
|
||||
func (b *Bar) Text(s string) {
|
||||
b.text = s
|
||||
}
|
||||
|
||||
// Value sets the value.
|
||||
func (b *Bar) Value(n float64) {
|
||||
if n > b.Total {
|
||||
panic("Bar update value cannot be greater than the total")
|
||||
}
|
||||
b.value = n
|
||||
}
|
||||
|
||||
// ValueInt sets the value.
|
||||
func (b *Bar) ValueInt(n int) {
|
||||
b.Value(float64(n))
|
||||
}
|
||||
|
||||
// Percent returns the percentage
|
||||
func (b *Bar) percent() float64 {
|
||||
return (b.value / b.Total) * 100
|
||||
}
|
||||
|
||||
// Bar returns the progress bar string.
|
||||
func (b *Bar) bar() string {
|
||||
p := b.value / b.Total
|
||||
filled := math.Ceil(float64(b.Width) * p)
|
||||
empty := math.Floor(float64(b.Width) - filled)
|
||||
s := b.StartDelimiter
|
||||
s += strings.Repeat(b.Filled, int(filled))
|
||||
s += strings.Repeat(b.Empty, int(empty))
|
||||
s += b.EndDelimiter
|
||||
return s
|
||||
}
|
||||
|
||||
// String returns the progress bar.
|
||||
func (b *Bar) String() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
data := struct {
|
||||
Value float64
|
||||
Total float64
|
||||
Percent float64
|
||||
StartDelimiter string
|
||||
EndDelimiter string
|
||||
Bar string
|
||||
Text string
|
||||
}{
|
||||
Value: b.value,
|
||||
Text: b.text,
|
||||
StartDelimiter: b.StartDelimiter,
|
||||
EndDelimiter: b.EndDelimiter,
|
||||
Percent: b.percent(),
|
||||
Bar: b.bar(),
|
||||
}
|
||||
|
||||
if err := b.tmpl.Execute(&buf, data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// WriteTo writes the progress bar to w.
|
||||
func (b *Bar) WriteTo(w io.Writer) (int64, error) {
|
||||
s := fmt.Sprintf("\r %s ", b.String())
|
||||
_, err := io.WriteString(w, s)
|
||||
return int64(len(s)), err
|
||||
}
|
||||
|
||||
// Template for rendering. This method will panic if the template fails to parse.
|
||||
func (b *Bar) Template(s string) {
|
||||
t, err := template.New("").Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.tmpl = t
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/database"
|
||||
"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/utils"
|
||||
)
|
||||
|
||||
func formatSpeed(speed float64) string {
|
||||
if speed < 1000 {
|
||||
return fmt.Sprintf("%.2f Kbit/s", speed)
|
||||
} else if speed < 1000*1000 {
|
||||
return fmt.Sprintf("%.2f Mbit/s", float32(speed)/1000)
|
||||
} else if speed < 1000*1000*1000 {
|
||||
return fmt.Sprintf("%.2f Gbit/s", float32(speed)/(1000*1000))
|
||||
}
|
||||
// WTF, you crazy?
|
||||
return fmt.Sprintf("%.2f Tbit/s", float32(speed)/(1000*1000*1000))
|
||||
}
|
||||
|
||||
func formatSize(size float64) string {
|
||||
if size < 1024 {
|
||||
return fmt.Sprintf("%.1fK", size)
|
||||
} else if size < 1024*1024 {
|
||||
return fmt.Sprintf("%.1fM", size/1024.0)
|
||||
} else if size < 1024*1024*1024 {
|
||||
return fmt.Sprintf("%.1fG", size/(1024.0*1024.0))
|
||||
}
|
||||
// WTF, you crazy?
|
||||
return fmt.Sprintf("%.1fT", size/(1024*1024*1024))
|
||||
}
|
||||
|
||||
var summarizers = map[string]func(uint64, uint64, string) []string{
|
||||
"websites": func(totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%d tested", totalCount),
|
||||
fmt.Sprintf("%d blocked", anomalyCount),
|
||||
"",
|
||||
}
|
||||
},
|
||||
"performance": func(totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
var tk database.PerformanceTestKeys
|
||||
if err := json.Unmarshal([]byte(ss), &tk); err != nil {
|
||||
return nil
|
||||
}
|
||||
return []string{
|
||||
fmt.Sprintf("Download: %s", formatSpeed(tk.Download)),
|
||||
fmt.Sprintf("Upload: %s", formatSpeed(tk.Upload)),
|
||||
fmt.Sprintf("Ping: %.2fms", tk.Ping),
|
||||
}
|
||||
},
|
||||
"im": func(totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%d tested", totalCount),
|
||||
fmt.Sprintf("%d blocked", anomalyCount),
|
||||
"",
|
||||
}
|
||||
},
|
||||
"middlebox": func(totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("Detected: %v", anomalyCount > 0),
|
||||
"",
|
||||
"",
|
||||
}
|
||||
},
|
||||
"circumvention": func(totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%d tested", totalCount),
|
||||
fmt.Sprintf("%d blocked", anomalyCount),
|
||||
"",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func makeSummary(name string, totalCount uint64, anomalyCount uint64, ss string) []string {
|
||||
return summarizers[name](totalCount, anomalyCount, ss)
|
||||
}
|
||||
|
||||
func logResultItem(w io.Writer, f log.Fields) error {
|
||||
colWidth := 24
|
||||
rID := f.Get("id").(int64)
|
||||
name := f.Get("name").(string)
|
||||
isDone := f.Get("is_done").(bool)
|
||||
startTime := f.Get("start_time").(time.Time)
|
||||
networkName := f.Get("network_name").(string)
|
||||
asn := fmt.Sprintf("AS%d (%s)", f.Get("asn").(uint), f.Get("network_country_code").(string))
|
||||
//runtime := f.Get("runtime").(float64)
|
||||
//dataUsageUp := f.Get("dataUsageUp").(int64)
|
||||
//dataUsageDown := f.Get("dataUsageDown").(int64)
|
||||
index := f.Get("index").(int)
|
||||
totalCount := f.Get("total_count").(int)
|
||||
if index == 0 {
|
||||
fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth*2+2)+"┓\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "┢"+strings.Repeat("━", colWidth*2+2)+"┪\n")
|
||||
}
|
||||
|
||||
firstRow := utils.RightPad(fmt.Sprintf("#%d - %s", rID, startTime.Format(time.RFC822)), colWidth*2)
|
||||
fmt.Fprintf(w, "┃ "+firstRow+" ┃\n")
|
||||
fmt.Fprintf(w, "┡"+strings.Repeat("━", colWidth*2+2)+"┩\n")
|
||||
|
||||
summary := makeSummary(name,
|
||||
f.Get("measurement_count").(uint64),
|
||||
f.Get("measurement_anomaly_count").(uint64),
|
||||
f.Get("test_keys").(string))
|
||||
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n",
|
||||
utils.RightPad(name, colWidth),
|
||||
utils.RightPad(summary[0], colWidth)))
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n",
|
||||
utils.RightPad(networkName, colWidth),
|
||||
utils.RightPad(summary[1], colWidth)))
|
||||
fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n",
|
||||
utils.RightPad(asn, colWidth),
|
||||
utils.RightPad(summary[2], colWidth)))
|
||||
|
||||
if index == totalCount-1 {
|
||||
if isDone == true {
|
||||
fmt.Fprintf(w, "└┬──────────────┬─────────────┬───────────────────┬┘\n")
|
||||
} else {
|
||||
// We want the incomplete section to not have a footer
|
||||
fmt.Fprintf(w, "└──────────────────────────────────────────────────┘\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logResultSummary(w io.Writer, f log.Fields) error {
|
||||
|
||||
networks := f.Get("total_networks").(int64)
|
||||
tests := f.Get("total_tests").(int64)
|
||||
dataUp := f.Get("total_data_usage_up").(float64)
|
||||
dataDown := f.Get("total_data_usage_down").(float64)
|
||||
if tests == 0 {
|
||||
fmt.Fprintf(w, "No results\n")
|
||||
fmt.Fprintf(w, "Try running:\n")
|
||||
fmt.Fprintf(w, " ooniprobe run websites\n")
|
||||
return nil
|
||||
}
|
||||
// └┬──────────────┬─────────────┬───────────────┬
|
||||
fmt.Fprintf(w, " │ %s │ %s │ %s │\n",
|
||||
utils.RightPad(fmt.Sprintf("%d tests", tests), 12),
|
||||
utils.RightPad(fmt.Sprintf("%d nets", networks), 11),
|
||||
utils.RightPad(fmt.Sprintf("⬆ %s ⬇ %s", formatSize(dataUp), formatSize(dataDown)), 17))
|
||||
fmt.Fprintf(w, " └──────────────┴─────────────┴───────────────────┘\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user