...

Source file src/github.com/onsi/ginkgo/v2/table_dsl.go

Documentation: github.com/onsi/ginkgo/v2

     1  package ginkgo
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/onsi/ginkgo/v2/internal"
    10  	"github.com/onsi/ginkgo/v2/types"
    11  )
    12  
    13  /*
    14  The EntryDescription decorator allows you to pass a format string to DescribeTable() and Entry().  This format string is used to generate entry names via:
    15  
    16  	fmt.Sprintf(formatString, parameters...)
    17  
    18  where parameters are the parameters passed into the entry.
    19  
    20  When passed into an Entry the EntryDescription is used to generate the name or that entry.  When passed to DescribeTable, the EntryDescription is used to generate the names for any entries that have `nil` descriptions.
    21  
    22  You can learn more about generating EntryDescriptions here: https://onsi.github.io/ginkgo/#generating-entry-descriptions
    23  */
    24  type EntryDescription string
    25  
    26  func (ed EntryDescription) render(args ...interface{}) string {
    27  	return fmt.Sprintf(string(ed), args...)
    28  }
    29  
    30  /*
    31  DescribeTable describes a table-driven spec.
    32  
    33  For example:
    34  
    35  	DescribeTable("a simple table",
    36  	    func(x int, y int, expected bool) {
    37  	        Ω(x > y).Should(Equal(expected))
    38  	    },
    39  	    Entry("x > y", 1, 0, true),
    40  	    Entry("x == y", 0, 0, false),
    41  	    Entry("x < y", 0, 1, false),
    42  	)
    43  
    44  You can learn more about DescribeTable here: https://onsi.github.io/ginkgo/#table-specs
    45  And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-specs-patterns
    46  */
    47  func DescribeTable(description string, args ...interface{}) bool {
    48  	GinkgoHelper()
    49  	generateTable(description, false, args...)
    50  	return true
    51  }
    52  
    53  /*
    54  You can focus a table with `FDescribeTable`.  This is equivalent to `FDescribe`.
    55  */
    56  func FDescribeTable(description string, args ...interface{}) bool {
    57  	GinkgoHelper()
    58  	args = append(args, internal.Focus)
    59  	generateTable(description, false, args...)
    60  	return true
    61  }
    62  
    63  /*
    64  You can mark a table as pending with `PDescribeTable`.  This is equivalent to `PDescribe`.
    65  */
    66  func PDescribeTable(description string, args ...interface{}) bool {
    67  	GinkgoHelper()
    68  	args = append(args, internal.Pending)
    69  	generateTable(description, false, args...)
    70  	return true
    71  }
    72  
    73  /*
    74  You can mark a table as pending with `XDescribeTable`.  This is equivalent to `XDescribe`.
    75  */
    76  var XDescribeTable = PDescribeTable
    77  
    78  /*
    79  DescribeTableSubtree describes a table-driven spec that generates a set of tests for each entry.
    80  
    81  For example:
    82  
    83  	DescribeTableSubtree("a subtree table",
    84  	    func(url string, code int, message string) {
    85  			var resp *http.Response
    86  			BeforeEach(func() {
    87  				var err error
    88  				resp, err = http.Get(url)
    89  				Expect(err).NotTo(HaveOccurred())
    90  				DeferCleanup(resp.Body.Close)
    91  			})
    92  
    93  			It("should return the expected status code", func() {
    94  				Expect(resp.StatusCode).To(Equal(code))
    95  			})
    96  
    97  			It("should return the expected message", func() {
    98  				body, err := ioutil.ReadAll(resp.Body)
    99  				Expect(err).NotTo(HaveOccurred())
   100  				Expect(string(body)).To(Equal(message))
   101  			})
   102  	    },
   103  	    Entry("default response", "example.com/response", http.StatusOK, "hello world"),
   104  	    Entry("missing response", "example.com/missing", http.StatusNotFound, "wat?"),
   105  	)
   106  
   107  Note that you **must** place define an It inside the body function.
   108  
   109  You can learn more about DescribeTableSubtree here: https://onsi.github.io/ginkgo/#table-specs
   110  And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-specs-patterns
   111  */
   112  func DescribeTableSubtree(description string, args ...interface{}) bool {
   113  	GinkgoHelper()
   114  	generateTable(description, true, args...)
   115  	return true
   116  }
   117  
   118  /*
   119  You can focus a table with `FDescribeTableSubtree`.  This is equivalent to `FDescribe`.
   120  */
   121  func FDescribeTableSubtree(description string, args ...interface{}) bool {
   122  	GinkgoHelper()
   123  	args = append(args, internal.Focus)
   124  	generateTable(description, true, args...)
   125  	return true
   126  }
   127  
   128  /*
   129  You can mark a table as pending with `PDescribeTableSubtree`.  This is equivalent to `PDescribe`.
   130  */
   131  func PDescribeTableSubtree(description string, args ...interface{}) bool {
   132  	GinkgoHelper()
   133  	args = append(args, internal.Pending)
   134  	generateTable(description, true, args...)
   135  	return true
   136  }
   137  
   138  /*
   139  You can mark a table as pending with `XDescribeTableSubtree`.  This is equivalent to `XDescribe`.
   140  */
   141  var XDescribeTableSubtree = PDescribeTableSubtree
   142  
   143  /*
   144  TableEntry represents an entry in a table test.  You generally use the `Entry` constructor.
   145  */
   146  type TableEntry struct {
   147  	description  interface{}
   148  	decorations  []interface{}
   149  	parameters   []interface{}
   150  	codeLocation types.CodeLocation
   151  }
   152  
   153  /*
   154  Entry constructs a TableEntry.
   155  
   156  The first argument is a description.  This can be a string, a function that accepts the parameters passed to the TableEntry and returns a string, an EntryDescription format string, or nil.  If nil is provided then the name of the Entry is derived using the table-level entry description.
   157  Subsequent arguments accept any Ginkgo decorators.  These are filtered out and the remaining arguments are passed into the Spec function associated with the table.
   158  
   159  Each Entry ends up generating an individual Ginkgo It.  The body of the it is the Table Body function with the Entry parameters passed in.
   160  
   161  If you want to generate interruptible specs simply write a Table function that accepts a SpecContext as its first argument.  You can then decorate individual Entrys with the NodeTimeout and SpecTimeout decorators.
   162  
   163  You can learn more about Entry here: https://onsi.github.io/ginkgo/#table-specs
   164  */
   165  func Entry(description interface{}, args ...interface{}) TableEntry {
   166  	GinkgoHelper()
   167  	decorations, parameters := internal.PartitionDecorations(args...)
   168  	return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
   169  }
   170  
   171  /*
   172  You can focus a particular entry with FEntry.  This is equivalent to FIt.
   173  */
   174  func FEntry(description interface{}, args ...interface{}) TableEntry {
   175  	GinkgoHelper()
   176  	decorations, parameters := internal.PartitionDecorations(args...)
   177  	decorations = append(decorations, internal.Focus)
   178  	return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
   179  }
   180  
   181  /*
   182  You can mark a particular entry as pending with PEntry.  This is equivalent to PIt.
   183  */
   184  func PEntry(description interface{}, args ...interface{}) TableEntry {
   185  	GinkgoHelper()
   186  	decorations, parameters := internal.PartitionDecorations(args...)
   187  	decorations = append(decorations, internal.Pending)
   188  	return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
   189  }
   190  
   191  /*
   192  You can mark a particular entry as pending with XEntry.  This is equivalent to XIt.
   193  */
   194  var XEntry = PEntry
   195  
   196  var contextType = reflect.TypeOf(new(context.Context)).Elem()
   197  var specContextType = reflect.TypeOf(new(SpecContext)).Elem()
   198  
   199  func generateTable(description string, isSubtree bool, args ...interface{}) {
   200  	GinkgoHelper()
   201  	cl := types.NewCodeLocation(0)
   202  	containerNodeArgs := []interface{}{cl}
   203  
   204  	entries := []TableEntry{}
   205  	var internalBody interface{}
   206  	var internalBodyType reflect.Type
   207  
   208  	var tableLevelEntryDescription interface{}
   209  	tableLevelEntryDescription = func(args ...interface{}) string {
   210  		out := []string{}
   211  		for _, arg := range args {
   212  			out = append(out, fmt.Sprint(arg))
   213  		}
   214  		return "Entry: " + strings.Join(out, ", ")
   215  	}
   216  
   217  	if len(args) == 1 {
   218  		exitIfErr(types.GinkgoErrors.MissingParametersForTableFunction(cl))
   219  	}
   220  
   221  	for i, arg := range args {
   222  		switch t := reflect.TypeOf(arg); {
   223  		case t == nil:
   224  			exitIfErr(types.GinkgoErrors.IncorrectParameterTypeForTable(i, "nil", cl))
   225  		case t == reflect.TypeOf(TableEntry{}):
   226  			entries = append(entries, arg.(TableEntry))
   227  		case t == reflect.TypeOf([]TableEntry{}):
   228  			entries = append(entries, arg.([]TableEntry)...)
   229  		case t == reflect.TypeOf(EntryDescription("")):
   230  			tableLevelEntryDescription = arg.(EntryDescription).render
   231  		case t.Kind() == reflect.Func && t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(""):
   232  			tableLevelEntryDescription = arg
   233  		case t.Kind() == reflect.Func:
   234  			if internalBody != nil {
   235  				exitIfErr(types.GinkgoErrors.MultipleEntryBodyFunctionsForTable(cl))
   236  			}
   237  			internalBody = arg
   238  			internalBodyType = reflect.TypeOf(internalBody)
   239  		default:
   240  			containerNodeArgs = append(containerNodeArgs, arg)
   241  		}
   242  	}
   243  
   244  	containerNodeArgs = append(containerNodeArgs, func() {
   245  		for _, entry := range entries {
   246  			var err error
   247  			entry := entry
   248  			var description string
   249  			switch t := reflect.TypeOf(entry.description); {
   250  			case t == nil:
   251  				err = validateParameters(tableLevelEntryDescription, entry.parameters, "Entry Description function", entry.codeLocation, false)
   252  				if err == nil {
   253  					description = invokeFunction(tableLevelEntryDescription, entry.parameters)[0].String()
   254  				}
   255  			case t == reflect.TypeOf(EntryDescription("")):
   256  				description = entry.description.(EntryDescription).render(entry.parameters...)
   257  			case t == reflect.TypeOf(""):
   258  				description = entry.description.(string)
   259  			case t.Kind() == reflect.Func && t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(""):
   260  				err = validateParameters(entry.description, entry.parameters, "Entry Description function", entry.codeLocation, false)
   261  				if err == nil {
   262  					description = invokeFunction(entry.description, entry.parameters)[0].String()
   263  				}
   264  			default:
   265  				err = types.GinkgoErrors.InvalidEntryDescription(entry.codeLocation)
   266  			}
   267  
   268  			internalNodeArgs := []interface{}{entry.codeLocation}
   269  			internalNodeArgs = append(internalNodeArgs, entry.decorations...)
   270  
   271  			hasContext := false
   272  			if internalBodyType.NumIn() > 0. {
   273  				if internalBodyType.In(0).Implements(specContextType) {
   274  					hasContext = true
   275  				} else if internalBodyType.In(0).Implements(contextType) && (len(entry.parameters) == 0 || !reflect.TypeOf(entry.parameters[0]).Implements(contextType)) {
   276  					hasContext = true
   277  				}
   278  			}
   279  
   280  			if err == nil {
   281  				err = validateParameters(internalBody, entry.parameters, "Table Body function", entry.codeLocation, hasContext)
   282  			}
   283  
   284  			if hasContext {
   285  				internalNodeArgs = append(internalNodeArgs, func(c SpecContext) {
   286  					if err != nil {
   287  						panic(err)
   288  					}
   289  					invokeFunction(internalBody, append([]interface{}{c}, entry.parameters...))
   290  				})
   291  				if isSubtree {
   292  					exitIfErr(types.GinkgoErrors.ContextsCannotBeUsedInSubtreeTables(cl))
   293  				}
   294  			} else {
   295  				internalNodeArgs = append(internalNodeArgs, func() {
   296  					if err != nil {
   297  						panic(err)
   298  					}
   299  					invokeFunction(internalBody, entry.parameters)
   300  				})
   301  			}
   302  
   303  			internalNodeType := types.NodeTypeIt
   304  			if isSubtree {
   305  				internalNodeType = types.NodeTypeContainer
   306  			}
   307  
   308  			pushNode(internal.NewNode(deprecationTracker, internalNodeType, description, internalNodeArgs...))
   309  		}
   310  	})
   311  
   312  	pushNode(internal.NewNode(deprecationTracker, types.NodeTypeContainer, description, containerNodeArgs...))
   313  }
   314  
   315  func invokeFunction(function interface{}, parameters []interface{}) []reflect.Value {
   316  	inValues := make([]reflect.Value, len(parameters))
   317  
   318  	funcType := reflect.TypeOf(function)
   319  	limit := funcType.NumIn()
   320  	if funcType.IsVariadic() {
   321  		limit = limit - 1
   322  	}
   323  
   324  	for i := 0; i < limit && i < len(parameters); i++ {
   325  		inValues[i] = computeValue(parameters[i], funcType.In(i))
   326  	}
   327  
   328  	if funcType.IsVariadic() {
   329  		variadicType := funcType.In(limit).Elem()
   330  		for i := limit; i < len(parameters); i++ {
   331  			inValues[i] = computeValue(parameters[i], variadicType)
   332  		}
   333  	}
   334  
   335  	return reflect.ValueOf(function).Call(inValues)
   336  }
   337  
   338  func validateParameters(function interface{}, parameters []interface{}, kind string, cl types.CodeLocation, hasContext bool) error {
   339  	funcType := reflect.TypeOf(function)
   340  	limit := funcType.NumIn()
   341  	offset := 0
   342  	if hasContext {
   343  		limit = limit - 1
   344  		offset = 1
   345  	}
   346  	if funcType.IsVariadic() {
   347  		limit = limit - 1
   348  	}
   349  	if len(parameters) < limit {
   350  		return types.GinkgoErrors.TooFewParametersToTableFunction(limit, len(parameters), kind, cl)
   351  	}
   352  	if len(parameters) > limit && !funcType.IsVariadic() {
   353  		return types.GinkgoErrors.TooManyParametersToTableFunction(limit, len(parameters), kind, cl)
   354  	}
   355  	var i = 0
   356  	for ; i < limit; i++ {
   357  		actual := reflect.TypeOf(parameters[i])
   358  		expected := funcType.In(i + offset)
   359  		if !(actual == nil) && !actual.AssignableTo(expected) {
   360  			return types.GinkgoErrors.IncorrectParameterTypeToTableFunction(i+1, expected, actual, kind, cl)
   361  		}
   362  	}
   363  	if funcType.IsVariadic() {
   364  		expected := funcType.In(limit + offset).Elem()
   365  		for ; i < len(parameters); i++ {
   366  			actual := reflect.TypeOf(parameters[i])
   367  			if !(actual == nil) && !actual.AssignableTo(expected) {
   368  				return types.GinkgoErrors.IncorrectVariadicParameterTypeToTableFunction(expected, actual, kind, cl)
   369  			}
   370  		}
   371  	}
   372  
   373  	return nil
   374  }
   375  
   376  func computeValue(parameter interface{}, t reflect.Type) reflect.Value {
   377  	if parameter == nil {
   378  		return reflect.Zero(t)
   379  	} else {
   380  		return reflect.ValueOf(parameter)
   381  	}
   382  }
   383  

View as plain text