test table node and events fired
Этот коммит содержится в:
		
							родитель
							
								
									00fab00e22
								
							
						
					
					
						коммит
						49130c8b08
					
				
					 9 изменённых файлов: 140 добавлений и 70 удалений
				
			
		
							
								
								
									
										34
									
								
								features/events.feature
									
										
									
									
									
										Обычный файл
									
								
							
							
						
						
									
										34
									
								
								features/events.feature
									
										
									
									
									
										Обычный файл
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
Feature: suite events
 | 
			
		||||
  In order to run tasks before and after important events
 | 
			
		||||
  As a test suite
 | 
			
		||||
  I need to provide a way to hook into these events
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
    Given I'm listening to suite events
 | 
			
		||||
 | 
			
		||||
  Scenario: triggers before scenario event
 | 
			
		||||
    Given a feature path "features/load_features.feature:6"
 | 
			
		||||
    When I run feature suite
 | 
			
		||||
    Then there was event triggered before scenario "load features within path"
 | 
			
		||||
 | 
			
		||||
  Scenario: triggers appropriate events for a single scenario
 | 
			
		||||
    Given a feature path "features/load_features.feature:6"
 | 
			
		||||
    When I run feature suite
 | 
			
		||||
    Then these events had to be fired for a number of times:
 | 
			
		||||
      | BeforeSuite    | 1 |
 | 
			
		||||
      | BeforeScenario | 1 |
 | 
			
		||||
      | BeforeStep     | 3 |
 | 
			
		||||
      | AfterStep      | 3 |
 | 
			
		||||
      | AfterScenario  | 1 |
 | 
			
		||||
      | AfterSuite     | 1 |
 | 
			
		||||
 | 
			
		||||
  Scenario: triggers appropriate events whole feature
 | 
			
		||||
    Given a feature path "features/load_features.feature"
 | 
			
		||||
    When I run feature suite
 | 
			
		||||
    Then these events had to be fired for a number of times:
 | 
			
		||||
      | BeforeSuite    | 1  |
 | 
			
		||||
      | BeforeScenario | 4  |
 | 
			
		||||
      | BeforeStep     | 13 |
 | 
			
		||||
      | AfterStep      | 13 |
 | 
			
		||||
      | AfterScenario  | 4  |
 | 
			
		||||
      | AfterSuite     | 1  |
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
Feature: suite hooks
 | 
			
		||||
  In order to run tasks before and after important events
 | 
			
		||||
  As a test suite
 | 
			
		||||
  I need to provide a way to hook into these events
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
    Given I'm listening to suite events
 | 
			
		||||
 | 
			
		||||
  Scenario: triggers before scenario hook
 | 
			
		||||
    Given a feature path "features/load_features.feature:6"
 | 
			
		||||
    When I run feature suite
 | 
			
		||||
    Then there was event triggered before scenario "load features within path"
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ Feature: load features
 | 
			
		|||
    When I parse features
 | 
			
		||||
    Then I should have 2 feature files:
 | 
			
		||||
      """
 | 
			
		||||
      features/hooks.feature
 | 
			
		||||
      features/events.feature
 | 
			
		||||
      features/load_features.feature
 | 
			
		||||
      """
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,10 +27,10 @@ Feature: load features
 | 
			
		|||
 | 
			
		||||
  Scenario: load a number of feature files
 | 
			
		||||
    Given a feature path "features/load_features.feature"
 | 
			
		||||
    And a feature path "features/hooks.feature"
 | 
			
		||||
    And a feature path "features/events.feature"
 | 
			
		||||
    When I parse features
 | 
			
		||||
    Then I should have 2 feature files:
 | 
			
		||||
      """
 | 
			
		||||
      features/load_features.feature
 | 
			
		||||
      features/hooks.feature
 | 
			
		||||
      features/events.feature
 | 
			
		||||
      """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ type pretty struct {
 | 
			
		|||
	commentPos     int
 | 
			
		||||
	doneBackground bool
 | 
			
		||||
	background     *gherkin.Background
 | 
			
		||||
	scenario       *gherkin.Scenario
 | 
			
		||||
 | 
			
		||||
	// summary
 | 
			
		||||
	started   time.Time
 | 
			
		||||
| 
						 | 
				
			
			@ -38,20 +39,6 @@ func (f *pretty) line(tok *gherkin.Token) string {
 | 
			
		|||
	return cl(fmt.Sprintf("# %s:%d", f.feature.Path, tok.Line), black)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// checks whether it should not print a background step once again
 | 
			
		||||
func (f *pretty) canPrintStep(step *gherkin.Step) bool {
 | 
			
		||||
	if f.background == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if step.Background == nil {
 | 
			
		||||
		f.doneBackground = true
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return !f.doneBackground
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Node takes a gherkin node for formatting
 | 
			
		||||
func (f *pretty) Node(node interface{}) {
 | 
			
		||||
	switch t := node.(type) {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,12 +48,15 @@ func (f *pretty) Node(node interface{}) {
 | 
			
		|||
			fmt.Println("")
 | 
			
		||||
		}
 | 
			
		||||
		f.feature = t
 | 
			
		||||
		f.doneBackground = false
 | 
			
		||||
		f.scenario = nil
 | 
			
		||||
		f.background = nil
 | 
			
		||||
		f.features = append(f.features, t)
 | 
			
		||||
		fmt.Println(bcl("Feature: ", white) + t.Title)
 | 
			
		||||
		fmt.Println(t.Description)
 | 
			
		||||
	case *gherkin.Background:
 | 
			
		||||
		// do not repeat background for the same feature
 | 
			
		||||
		if f.background == nil && f.scenario == nil {
 | 
			
		||||
			f.background = t
 | 
			
		||||
			// determine comment position based on step length
 | 
			
		||||
			f.commentPos = len(t.Token.Text)
 | 
			
		||||
			for _, step := range t.Steps {
 | 
			
		||||
| 
						 | 
				
			
			@ -74,12 +64,11 @@ func (f *pretty) Node(node interface{}) {
 | 
			
		|||
					f.commentPos = len(step.Token.Text)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		// do not repeat background
 | 
			
		||||
		if !f.doneBackground {
 | 
			
		||||
			f.background = t
 | 
			
		||||
			// print background node
 | 
			
		||||
			fmt.Println("\n" + s(t.Token.Indent) + bcl("Background:", white))
 | 
			
		||||
		}
 | 
			
		||||
	case *gherkin.Scenario:
 | 
			
		||||
		f.scenario = t
 | 
			
		||||
		// determine comment position based on step length
 | 
			
		||||
		f.commentPos = len(t.Token.Text)
 | 
			
		||||
		for _, step := range t.Steps {
 | 
			
		||||
| 
						 | 
				
			
			@ -180,7 +169,8 @@ func (f *pretty) printStep(stepAction interface{}) {
 | 
			
		|||
		fatal(fmt.Errorf("unexpected step type received: %T", typ))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !f.canPrintStep(step) {
 | 
			
		||||
	// do not print background more than once
 | 
			
		||||
	if f.scenario == nil && step.Background != f.background {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -225,11 +215,33 @@ func (f *pretty) printStep(stepAction interface{}) {
 | 
			
		|||
		fmt.Println(cl(step.PyString.Raw, c))
 | 
			
		||||
		fmt.Println(s(step.Token.Indent+2) + cl(`"""`, c))
 | 
			
		||||
	}
 | 
			
		||||
	if step.Table != nil {
 | 
			
		||||
		f.printTable(step.Table, c)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(s(step.Token.Indent) + bcl(err, red))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// print table with aligned table cells
 | 
			
		||||
func (f *pretty) printTable(t *gherkin.Table, c color) {
 | 
			
		||||
	var longest = make([]int, len(t.Rows[0]))
 | 
			
		||||
	var cols = make([]string, len(t.Rows[0]))
 | 
			
		||||
	for _, row := range t.Rows {
 | 
			
		||||
		for i, col := range row {
 | 
			
		||||
			if longest[i] < len(col) {
 | 
			
		||||
				longest[i] = len(col)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, row := range t.Rows {
 | 
			
		||||
		for i, col := range row {
 | 
			
		||||
			cols[i] = col + s(longest[i]-len(col))
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println(s(t.Token.Indent) + cl("| "+strings.Join(cols, " | ")+" |", c))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Passed is called to represent a passed step
 | 
			
		||||
func (f *pretty) Passed(step *gherkin.Step, match *stepMatchHandler) {
 | 
			
		||||
	s := &passed{step: step, handler: match}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ type Table struct {
 | 
			
		|||
	*Token
 | 
			
		||||
	OutlineScenario *Scenario
 | 
			
		||||
	Step            *Step
 | 
			
		||||
	rows            [][]string
 | 
			
		||||
	Rows            [][]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var allSteps = []TokenType{
 | 
			
		||||
| 
						 | 
				
			
			@ -369,17 +369,17 @@ func (p *parser) parsePystring() (*PyString, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (p *parser) parseTable() (*Table, error) {
 | 
			
		||||
	tbl := &Table{}
 | 
			
		||||
	tbl := &Table{Token: p.peek()}
 | 
			
		||||
	for row := p.peek(); row.Type == TABLE_ROW; row = p.peek() {
 | 
			
		||||
		var cols []string
 | 
			
		||||
		for _, r := range strings.Split(strings.Trim(row.Value, "|"), "|") {
 | 
			
		||||
			cols = append(cols, strings.TrimFunc(r, unicode.IsSpace))
 | 
			
		||||
		}
 | 
			
		||||
		// ensure the same colum number for each row
 | 
			
		||||
		if len(tbl.rows) > 0 && len(tbl.rows[0]) != len(cols) {
 | 
			
		||||
		if len(tbl.Rows) > 0 && len(tbl.Rows[0]) != len(cols) {
 | 
			
		||||
			return tbl, p.err("table row has not the same number of columns compared to previous row", row.Line)
 | 
			
		||||
		}
 | 
			
		||||
		tbl.rows = append(tbl.rows, cols)
 | 
			
		||||
		tbl.Rows = append(tbl.Rows, cols)
 | 
			
		||||
		p.next() // jump over the peeked token
 | 
			
		||||
	}
 | 
			
		||||
	return tbl, nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,13 +25,13 @@ func (s *Scenario) assertExampleRow(t *testing.T, num int, cols ...string) {
 | 
			
		|||
	if s.Examples == nil {
 | 
			
		||||
		t.Fatalf("outline scenario '%s' has no examples", s.Title)
 | 
			
		||||
	}
 | 
			
		||||
	if len(s.Examples.rows) <= num {
 | 
			
		||||
	if len(s.Examples.Rows) <= num {
 | 
			
		||||
		t.Fatalf("outline scenario '%s' table has no row: %d", s.Title, num)
 | 
			
		||||
	}
 | 
			
		||||
	if len(s.Examples.rows[num]) != len(cols) {
 | 
			
		||||
	if len(s.Examples.Rows[num]) != len(cols) {
 | 
			
		||||
		t.Fatalf("outline scenario '%s' table row length, does not match expected: %d", s.Title, len(cols))
 | 
			
		||||
	}
 | 
			
		||||
	for i, col := range s.Examples.rows[num] {
 | 
			
		||||
	for i, col := range s.Examples.Rows[num] {
 | 
			
		||||
		if col != cols[i] {
 | 
			
		||||
			t.Fatalf("outline scenario '%s' table row %d, column %d - value '%s', does not match expected: %s", s.Title, num, i, col, cols[i])
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,13 +69,13 @@ func (s *Step) assertTableRow(t *testing.T, num int, cols ...string) {
 | 
			
		|||
	if s.Table == nil {
 | 
			
		||||
		t.Fatalf("step '%s %s' has no table", s.Type, s.Text)
 | 
			
		||||
	}
 | 
			
		||||
	if len(s.Table.rows) <= num {
 | 
			
		||||
	if len(s.Table.Rows) <= num {
 | 
			
		||||
		t.Fatalf("step '%s %s' table has no row: %d", s.Type, s.Text, num)
 | 
			
		||||
	}
 | 
			
		||||
	if len(s.Table.rows[num]) != len(cols) {
 | 
			
		||||
	if len(s.Table.Rows[num]) != len(cols) {
 | 
			
		||||
		t.Fatalf("step '%s %s' table row length, does not match expected: %d", s.Type, s.Text, len(cols))
 | 
			
		||||
	}
 | 
			
		||||
	for i, col := range s.Table.rows[num] {
 | 
			
		||||
	for i, col := range s.Table.Rows[num] {
 | 
			
		||||
		if col != cols[i] {
 | 
			
		||||
			t.Fatalf("step '%s %s' table row %d, column %d - value '%s', does not match expected: %s", s.Type, s.Text, num, i, col, cols[i])
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								suite.go
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								suite.go
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -11,9 +11,10 @@ import (
 | 
			
		|||
	"github.com/DATA-DOG/godog/gherkin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Status represents a step status
 | 
			
		||||
// Status represents a step or scenario status
 | 
			
		||||
type Status int
 | 
			
		||||
 | 
			
		||||
// step or scenario status constants
 | 
			
		||||
const (
 | 
			
		||||
	invalid Status = iota
 | 
			
		||||
	Passed
 | 
			
		||||
| 
						 | 
				
			
			@ -303,7 +304,7 @@ func (s *suite) runFeature(f *gherkin.Feature) {
 | 
			
		|||
			s.skipSteps(scenario.Steps)
 | 
			
		||||
		case status == Undefined:
 | 
			
		||||
			s.skipSteps(scenario.Steps)
 | 
			
		||||
		case status == invalid:
 | 
			
		||||
		case status == Passed || status == invalid:
 | 
			
		||||
			status = s.runSteps(scenario.Steps)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,24 +14,39 @@ type firedEvent struct {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
type suiteFeature struct {
 | 
			
		||||
	suite
 | 
			
		||||
	testedSuite *suite
 | 
			
		||||
	events      []*firedEvent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *suiteFeature) HandleBeforeScenario(scenario *gherkin.Scenario) {
 | 
			
		||||
	// reset whole suite with the state
 | 
			
		||||
	s.testedSuite = &suite{fmt: &testFormatter{}}
 | 
			
		||||
	// our tested suite will have the same context registered
 | 
			
		||||
	SuiteContext(s.testedSuite)
 | 
			
		||||
	// reset feature paths
 | 
			
		||||
	cfg.paths = []string{}
 | 
			
		||||
	// reset event stack
 | 
			
		||||
	// reset all fired events
 | 
			
		||||
	s.events = []*firedEvent{}
 | 
			
		||||
	// reset formatter, which collects all details
 | 
			
		||||
	s.fmt = &testFormatter{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *suiteFeature) iAmListeningToSuiteEvents(args ...*Arg) error {
 | 
			
		||||
	s.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{
 | 
			
		||||
			scenario,
 | 
			
		||||
		}})
 | 
			
		||||
	s.testedSuite.BeforeSuite(BeforeSuiteHandlerFunc(func() {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"BeforeSuite", []interface{}{}})
 | 
			
		||||
	}))
 | 
			
		||||
	s.testedSuite.AfterSuite(AfterSuiteHandlerFunc(func() {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}})
 | 
			
		||||
	}))
 | 
			
		||||
	s.testedSuite.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}})
 | 
			
		||||
	}))
 | 
			
		||||
	s.testedSuite.AfterScenario(AfterScenarioHandlerFunc(func(scenario *gherkin.Scenario, status Status) {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, status}})
 | 
			
		||||
	}))
 | 
			
		||||
	s.testedSuite.BeforeStep(BeforeStepHandlerFunc(func(step *gherkin.Step) {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}})
 | 
			
		||||
	}))
 | 
			
		||||
	s.testedSuite.AfterStep(AfterStepHandlerFunc(func(step *gherkin.Step, status Status) {
 | 
			
		||||
		s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, status}})
 | 
			
		||||
	}))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -42,17 +57,17 @@ func (s *suiteFeature) featurePath(args ...*Arg) error {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (s *suiteFeature) parseFeatures(args ...*Arg) (err error) {
 | 
			
		||||
	s.features, err = cfg.features()
 | 
			
		||||
	s.testedSuite.features, err = cfg.features()
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *suiteFeature) iShouldHaveNumFeatureFiles(args ...*Arg) error {
 | 
			
		||||
	if len(s.features) != args[0].Int() {
 | 
			
		||||
		return fmt.Errorf("expected %d features to be parsed, but have %d", args[0].Int(), len(s.features))
 | 
			
		||||
	if len(s.testedSuite.features) != args[0].Int() {
 | 
			
		||||
		return fmt.Errorf("expected %d features to be parsed, but have %d", args[0].Int(), len(s.testedSuite.features))
 | 
			
		||||
	}
 | 
			
		||||
	expected := args[1].PyString().Lines
 | 
			
		||||
	var actual []string
 | 
			
		||||
	for _, ft := range s.features {
 | 
			
		||||
	for _, ft := range s.testedSuite.features {
 | 
			
		||||
		actual = append(actual, ft.Path)
 | 
			
		||||
	}
 | 
			
		||||
	if len(expected) != len(actual) {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,13 +85,13 @@ func (s *suiteFeature) iRunFeatureSuite(args ...*Arg) error {
 | 
			
		|||
	if err := s.parseFeatures(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	s.run()
 | 
			
		||||
	s.testedSuite.run()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *suiteFeature) numScenariosRegistered(args ...*Arg) (err error) {
 | 
			
		||||
	var num int
 | 
			
		||||
	for _, ft := range s.features {
 | 
			
		||||
	for _, ft := range s.testedSuite.features {
 | 
			
		||||
		num += len(ft.Scenarios)
 | 
			
		||||
	}
 | 
			
		||||
	if num != args[0].Int() {
 | 
			
		||||
| 
						 | 
				
			
			@ -120,11 +135,28 @@ func (s *suiteFeature) thereWasEventTriggeredBeforeScenario(args ...*Arg) error
 | 
			
		|||
	return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, args[0].String(), `"`+strings.Join(found, `", "`)+`"`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SuiteContext(g Suite) {
 | 
			
		||||
	s := &suiteFeature{
 | 
			
		||||
		suite: suite{},
 | 
			
		||||
func (s *suiteFeature) theseEventsHadToBeFiredForNumberOfTimes(args ...*Arg) error {
 | 
			
		||||
	tbl := args[0].Table()
 | 
			
		||||
	if len(tbl.Rows[0]) != 2 {
 | 
			
		||||
		return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, row := range tbl.Rows {
 | 
			
		||||
		args := []*Arg{
 | 
			
		||||
			StepArgument(""), // ignored
 | 
			
		||||
			StepArgument(row[1]),
 | 
			
		||||
			StepArgument(row[0]),
 | 
			
		||||
		}
 | 
			
		||||
		if err := s.thereWereNumEventsFired(args...); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SuiteContext(g Suite) {
 | 
			
		||||
	s := &suiteFeature{}
 | 
			
		||||
 | 
			
		||||
	g.BeforeScenario(s)
 | 
			
		||||
 | 
			
		||||
	g.Step(
 | 
			
		||||
| 
						 | 
				
			
			@ -151,4 +183,7 @@ func SuiteContext(g Suite) {
 | 
			
		|||
	g.Step(
 | 
			
		||||
		regexp.MustCompile(`^there was event triggered before scenario "([^"]*)"$`),
 | 
			
		||||
		StepHandlerFunc(s.thereWasEventTriggeredBeforeScenario))
 | 
			
		||||
	g.Step(
 | 
			
		||||
		regexp.MustCompile(`^these events had to be fired for a number of times:$`),
 | 
			
		||||
		StepHandlerFunc(s.theseEventsHadToBeFiredForNumberOfTimes))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Загрузка…
	
	Создание таблицы
		
		Сослаться в новой задаче