<commit_before>package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

type testRunner struct {
	wg        sync.WaitGroup
	w         io.Writer
	r         io.Reader
	semaphore chan int

	sync.Mutex
	errors          []error
	stepsInLine     int
	scenarios       int
	scenariosPassed int
	steps           int
	stepsPassed     int
}

var flagConcurrencyLevel int

func init() {
	flag.IntVar(&flagConcurrencyLevel, "c", runtime.NumCPU(), "Concurrency level, defaults to number of CPUs")
	flag.IntVar(&flagConcurrencyLevel, "concurrency", runtime.NumCPU(), "Concurrency level, defaults to number of CPUs")
}

func main() {
	flag.Parse()
	t := NewTestRunner(flagConcurrencyLevel)
	start := time.Now()

	t.run()

	fmt.Println()
	for _, e := range t.errors {
		fmt.Println(e)
	}

	fmt.Printf("%d scenarios (%s)\n", t.scenarios, green(fmt.Sprintf("%d passed", t.scenariosPassed)))
	fmt.Printf("%d steps (%s)\n", t.steps, green(fmt.Sprintf("%d passed", t.stepsPassed)))
	fmt.Printf("Tests ran in: %s\n", time.Since(start))
}

func NewTestRunner(concurrencyLevel int) *testRunner {
	reader, writer := io.Pipe()
	return &testRunner{
		wg:              sync.WaitGroup{},
		w:               writer,
		r:               reader,
		stepsInLine:     0,
		scenarios:       0,
		scenariosPassed: 0,
		steps:           0,
		stepsPassed:     0,
		errors:          make([]error, 0),
		semaphore:       make(chan int, concurrencyLevel),
	}
}

func (t *testRunner) run() {
	features := features()
	for _, feature := range features {
		t.wg.Add(1)
		go t.executeTest(feature)
	}
	t.wg.Wait()
}

func (t *testRunner) executeTest(test string) {
	t.semaphore <- 1
	behat := exec.Command("./bin/behat", "-f", "progress", test)
	stdout, err := behat.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	go t.proccessOutput(stdout)
	err = behat.Start()
	if err != nil {
		log.Fatal(err)
	}
	err = behat.Wait()
	if err != nil {
		t.errors = append(t.errors, fmt.Errorf("TODO: handle std err output from behat: %s", err))
	}
	<-t.semaphore
	t.wg.Done()
}

func (t *testRunner) proccessOutput(out io.Reader) {
	colorMap := map[byte]func(string) string{
		'.': green,
		'-': cyan,
		'F': red,
		'U': yellow,
	}
	reader := bufio.NewReader(out)
	for {
		c, err := reader.ReadByte()
		switch {
		case c == '\n':
			// if we encounted two new lines in a row - steps have finished
			// and we try to parse information about runned scenarios and steps
			nextByte, err := reader.Peek(1)
			if err != nil {
				break
			}
			if nextByte[0] == '\n' {
				_, err = reader.ReadByte()
				for {
					line, err := reader.ReadBytes('\n')
					if err != nil {
						break
					}
					// TODO:
					// parse scenarios and steps info and store somewhere.
					// parse failed, skipped and undefined scenarios/steps
					if n, matched := parseSuiteInfo("scenario", line); matched {
						t.Lock()
						t.scenarios += n
						t.Unlock()
						if n, matched = parseSuiteInfo("passed", line); matched {
							t.Lock()
							t.scenariosPassed += n
							t.Unlock()
						}
					}

					if n, matched := parseSuiteInfo("step", line); matched {
						t.Lock()
						t.steps += n
						t.Unlock()
						if n, matched = parseSuiteInfo("passed", line); matched {
							t.Lock()
							t.stepsPassed += n
							t.Unlock()
						}
					}
				}
			}
			break
		case c == '.' || c == '-' || c == 'F' || c == 'U':
			if t.stepsInLine > 0 && t.stepsInLine%70 == 0 {
				fmt.Printf(" %d\n", t.stepsInLine)
			}
			fmt.Print(colorMap[c](string(c)))
			t.Lock()
			t.stepsInLine += 1
			t.Unlock()
			break
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Printf("Unknown error while proccessing output: %s", err)
			break
		}
	}
}

func features() []string {
	var features []string
	err := filepath.Walk("features", func(path string, file os.FileInfo, err error) error {
		if err == nil && !file.IsDir() {
			features = append(features, path)
		}
		return err
	})
	if err != nil {
		panic("failed to walk directory: " + err.Error())
	}
	return features
}

func parseSuiteInfo(s string, buf []byte) (n int, matched bool) {
	re := regexp.MustCompile("([0-9]+) " + s)
	match := re.FindString(string(buf))
	if match != "" {
		splitted := strings.Split(match, " ")
		n, _ := strconv.Atoi(splitted[0])
		return n, true
	}
	return 0, false
}

func green(s string) string {
	return fmt.Sprintf("\033[32m%s\033[0m", s)
}

func red(s string) string {
	return fmt.Sprintf("\033[31m%s\033[0m", s)
}

func cyan(s string) string {
	return fmt.Sprintf("\033[36m%s\033[0m", s)
}

func yellow(s string) string {
	return fmt.Sprintf("\033[33m%s\033[0m", s)
}<commit_msg>Added failed/skipped steps count, and changed summary printing part<commit_after>package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

type testRunner struct {
	wg        sync.WaitGroup
	w         io.Writer
	r         io.Reader
	semaphore chan int

	sync.Mutex
	errors      []error
	stepsInLine int
	summaryInfo *summary
}

type summary struct {
	sync.Mutex
	scenarios        int
	scenariosPassed  int
	scenariosFailed  int
	scenariosSkipped int
	steps            int
	stepsPassed      int
	stepsFailed      int
	stepsSkipped     int
}

var flagConcurrencyLevel int

func init() {
	flag.IntVar(&flagConcurrencyLevel, "c", runtime.NumCPU(), "Concurrency level, defaults to number of CPUs")
	flag.IntVar(&flagConcurrencyLevel, "concurrency", runtime.NumCPU(), "Concurrency level, defaults to number of CPUs")
}

func main() {
	flag.Parse()
	t := NewTestRunner(flagConcurrencyLevel)
	start := time.Now()
	t.run()
	t.summary()
	fmt.Printf("Tests ran in: %s\n", time.Since(start))
}

func NewTestRunner(concurrencyLevel int) *testRunner {
	reader, writer := io.Pipe()
	return &testRunner{
		wg:          sync.WaitGroup{},
		w:           writer,
		r:           reader,
		stepsInLine: 0,
		errors:      make([]error, 0),
		semaphore:   make(chan int, concurrencyLevel),
		summaryInfo: &summary{
			scenarios:        0,
			scenariosPassed:  0,
			scenariosFailed:  0,
			scenariosSkipped: 0,
			steps:            0,
			stepsPassed:      0,
			stepsFailed:      0,
			stepsSkipped:     0,
		},
	}
}

func (t *testRunner) summary() {
	fmt.Println()
	for _, e := range t.errors {
		fmt.Println(e)
	}
	fmt.Println(t.summaryInfo)
}

func (t *testRunner) run() {
	features := features()
	for _, feature := range features {
		t.wg.Add(1)
		go t.executeTest(feature)
	}
	t.wg.Wait()
}

func (t *testRunner) executeTest(test string) {
	t.semaphore <- 1
	behat := exec.Command("./bin/behat", "-f", "progress", test)
	stdout, err := behat.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	go t.proccessOutput(stdout)
	err = behat.Start()
	if err != nil {
		log.Fatal(err)
	}
	err = behat.Wait()
	if err != nil {
		t.errors = append(t.errors, fmt.Errorf("TODO: handle std err output from behat: %s", err))
	}
	<-t.semaphore
	t.wg.Done()
}

func (t *testRunner) proccessOutput(out io.Reader) {
	colorMap := map[byte]func(string) string{
		'.': green,
		'-': cyan,
		'F': red,
		'U': yellow,
	}
	reader := bufio.NewReader(out)
	for {
		c, err := reader.ReadByte()
		switch {
		case c == '\n':
			// if we encounted two new lines in a row - steps have finished
			// and we try to parse information about runned scenarios and steps
			nextByte, err := reader.Peek(1)
			if err != nil {
				break
			}
			if nextByte[0] == '\n' {
				_, err = reader.ReadByte()
				for {
					line, err := reader.ReadBytes('\n')
					if err != nil {
						break
					}
					t.summaryInfo.parseTestSummary(line)
				}
			}
			break
		case c == '.' || c == '-' || c == 'F' || c == 'U':
			t.Lock()
			if t.stepsInLine > 0 && t.stepsInLine%70 == 0 {
				fmt.Printf(" %d\n", t.stepsInLine)
			}
			fmt.Print(colorMap[c](string(c)))
			t.stepsInLine += 1
			t.Unlock()
			break
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Printf("Unknown error while proccessing output: %s", err)
			break
		}
	}
}

// TODO: add undefined steps
func (s *summary) parseTestSummary(line []byte) {
	if n, matched := parseSuiteInfo("scenario", line); matched {
		s.Lock()
		s.scenarios += n
		s.Unlock()
		if n, matched = parseSuiteInfo("passed", line); matched {
			s.Lock()
			s.scenariosPassed += n
			s.Unlock()
		}
		if n, matched = parseSuiteInfo("failed", line); matched {
			s.Lock()
			s.scenariosFailed += n
			s.Unlock()
		}
		if n, matched = parseSuiteInfo("skipped", line); matched {
			s.Lock()
			s.scenariosSkipped += n
			s.Unlock()
		}
	}

	if n, matched := parseSuiteInfo("step", line); matched {
		s.Lock()
		s.steps += n
		s.Unlock()
		if n, matched = parseSuiteInfo("passed", line); matched {
			s.Lock()
			s.stepsPassed += n
			s.Unlock()
		}
		if n, matched = parseSuiteInfo("failed", line); matched {
			s.Lock()
			s.stepsFailed += n
			s.Unlock()
		}
		if n, matched = parseSuiteInfo("skipped", line); matched {
			s.Lock()
			s.stepsSkipped += n
			s.Unlock()
		}
	}
}

func parseSuiteInfo(s string, buf []byte) (n int, matched bool) {
	re := regexp.MustCompile("([0-9]+) " + s)
	match := re.FindString(string(buf))
	if match != "" {
		splitted := strings.Split(match, " ")
		n, _ := strconv.Atoi(splitted[0])
		return n, true
	}
	return 0, false
}

func features() []string {
	var features []string
	err := filepath.Walk("features", func(path string, file os.FileInfo, err error) error {
		if err == nil && !file.IsDir() {
			features = append(features, path)
		}
		return err
	})
	if err != nil {
		panic("failed to walk directory: " + err.Error())
	}
	return features
}

func green(s string) string {
	return fmt.Sprintf("\033[32m%s\033[0m", s)
}

func red(s string) string {
	return fmt.Sprintf("\033[31m%s\033[0m", s)
}

func cyan(s string) string {
	return fmt.Sprintf("\033[36m%s\033[0m", s)
}

func yellow(s string) string {
	return fmt.Sprintf("\033[33m%s\033[0m", s)
}

func (s *summary) String() string {
	res := fmt.Sprintf("%d scenarios (%s", s.scenarios, green(fmt.Sprintf("%d passed", s.scenariosPassed)))
	if s.scenariosFailed > 0 {
		res += fmt.Sprintf(", %s", red(fmt.Sprintf("%d failed", s.scenariosFailed)))
	}
	if s.scenariosSkipped > 0 {
		res += fmt.Sprintf(", %s", cyan(fmt.Sprintf("%d skipped", s.scenariosSkipped)))
	}
	res += fmt.Sprintf(")\n")
	res += fmt.Sprintf("%d steps (%s", s.steps, green(fmt.Sprintf("%d passed", s.stepsPassed)))
	if s.stepsFailed > 0 {
		res += fmt.Sprintf(", %s", red(fmt.Sprintf("%d failed", s.stepsFailed)))
	}
	if s.stepsSkipped > 0 {
		res += fmt.Sprintf(", %s", cyan(fmt.Sprintf("%d skipped", s.stepsSkipped)))
	}
	res += fmt.Sprintf(")\n")
	return res
}
{'fraction_non_alphanumeric': 0.1630516080777861, 'fraction_numerical': 0.011032161555721766, 'mean_word_length': 2.942867674161445, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '427304', 'n_tokens_mistral': 4488, 'n_tokens_neox': 4006, 'n_words': 998}