github.com/golang/dep@v0.5.4/internal/test/test.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //lint:file-ignore U1000 unused fns we might want to use later.
     6  
     7  package test
     8  
     9  import (
    10  	"bytes"
    11  	"flag"
    12  	"fmt"
    13  	"go/format"
    14  	"io"
    15  	"io/ioutil"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"regexp"
    20  	"runtime"
    21  	"strings"
    22  	"sync"
    23  	"testing"
    24  
    25  	"github.com/pkg/errors"
    26  )
    27  
    28  var (
    29  	// ExeSuffix is the suffix of executable files; ".exe" on Windows.
    30  	ExeSuffix string
    31  	mu        sync.Mutex
    32  	// PrintLogs controls logging of test commands.
    33  	PrintLogs = flag.Bool("logs", false, "log stdin/stdout of test commands")
    34  	// UpdateGolden controls updating test fixtures.
    35  	UpdateGolden = flag.Bool("update", false, "update golden files")
    36  )
    37  
    38  const (
    39  	manifestName = "Gopkg.toml"
    40  	lockName     = "Gopkg.lock"
    41  )
    42  
    43  func init() {
    44  	switch runtime.GOOS {
    45  	case "windows":
    46  		ExeSuffix = ".exe"
    47  	}
    48  }
    49  
    50  // Helper with utilities for testing.
    51  type Helper struct {
    52  	t              *testing.T
    53  	temps          []string
    54  	wd             string
    55  	origWd         string
    56  	env            []string
    57  	tempdir        string
    58  	ran            bool
    59  	inParallel     bool
    60  	stdout, stderr bytes.Buffer
    61  }
    62  
    63  // NewHelper initializes a new helper for testing.
    64  func NewHelper(t *testing.T) *Helper {
    65  	wd, err := os.Getwd()
    66  	if err != nil {
    67  		panic(err)
    68  	}
    69  	return &Helper{t: t, origWd: wd}
    70  }
    71  
    72  // Must gives a fatal error if err is not nil.
    73  func (h *Helper) Must(err error) {
    74  	if err != nil {
    75  		h.t.Fatalf("%+v", err)
    76  	}
    77  }
    78  
    79  // check gives a test non-fatal error if err is not nil.
    80  func (h *Helper) check(err error) {
    81  	if err != nil {
    82  		h.t.Errorf("%+v", err)
    83  	}
    84  }
    85  
    86  // Parallel runs the test in parallel by calling t.Parallel.
    87  func (h *Helper) Parallel() {
    88  	if h.ran {
    89  		h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after run"))
    90  	}
    91  	if h.wd != "" {
    92  		h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after cd"))
    93  	}
    94  	for _, e := range h.env {
    95  		if strings.HasPrefix(e, "GOROOT=") || strings.HasPrefix(e, "GOPATH=") || strings.HasPrefix(e, "GOBIN=") {
    96  			val := e[strings.Index(e, "=")+1:]
    97  			if strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata") {
    98  				h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to parallel with testdata in environment (%s)", e))
    99  			}
   100  		}
   101  	}
   102  	h.inParallel = true
   103  	h.t.Parallel()
   104  }
   105  
   106  // pwd returns the current directory.
   107  func (h *Helper) pwd() string {
   108  	wd, err := os.Getwd()
   109  	if err != nil {
   110  		h.t.Fatalf("%+v", errors.Wrap(err, "could not get working directory"))
   111  	}
   112  	return wd
   113  }
   114  
   115  // Cd changes the current directory to the named directory. Note that
   116  // using this means that the test must not be run in parallel with any
   117  // other tests.
   118  func (h *Helper) Cd(dir string) {
   119  	if h.inParallel {
   120  		h.t.Fatalf("%+v", errors.New("internal testsuite error: changing directory when running in parallel"))
   121  	}
   122  	if h.wd == "" {
   123  		h.wd = h.pwd()
   124  	}
   125  	abs, err := filepath.Abs(dir)
   126  	if err == nil {
   127  		h.Setenv("PWD", abs)
   128  	}
   129  
   130  	err = os.Chdir(dir)
   131  	h.Must(errors.Wrapf(err, "Unable to cd to %s", dir))
   132  }
   133  
   134  // Setenv sets an environment variable to use when running the test go
   135  // command.
   136  func (h *Helper) Setenv(name, val string) {
   137  	if h.inParallel && (name == "GOROOT" || name == "GOPATH" || name == "GOBIN") && (strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata")) {
   138  		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to setenv with testdata (%s=%s) after parallel", name, val))
   139  	}
   140  	h.unsetenv(name)
   141  	h.env = append(h.env, name+"="+val)
   142  }
   143  
   144  // unsetenv removes an environment variable.
   145  func (h *Helper) unsetenv(name string) {
   146  	if h.env == nil {
   147  		h.env = append([]string(nil), os.Environ()...)
   148  	}
   149  	for i, v := range h.env {
   150  		if strings.HasPrefix(v, name+"=") {
   151  			h.env = append(h.env[:i], h.env[i+1:]...)
   152  			break
   153  		}
   154  	}
   155  }
   156  
   157  // DoRun runs the test go command, recording stdout and stderr and
   158  // returning exit status.
   159  func (h *Helper) DoRun(args []string) error {
   160  	if h.inParallel {
   161  		for _, arg := range args {
   162  			if strings.HasPrefix(arg, "testdata") || strings.HasPrefix(arg, "./testdata") {
   163  				h.t.Fatalf("%+v", errors.New("internal testsuite error: parallel run using testdata"))
   164  			}
   165  		}
   166  	}
   167  	if *PrintLogs {
   168  		h.t.Logf("running testdep %v", args)
   169  	}
   170  	var prog string
   171  	if h.wd == "" {
   172  		prog = "./testdep" + ExeSuffix
   173  	} else {
   174  		prog = filepath.Join(h.wd, "testdep"+ExeSuffix)
   175  	}
   176  	newargs := args
   177  	if args[0] != "check" {
   178  		newargs = append([]string{args[0], "-v"}, args[1:]...)
   179  	}
   180  
   181  	cmd := exec.Command(prog, newargs...)
   182  	h.stdout.Reset()
   183  	h.stderr.Reset()
   184  	cmd.Stdout = &h.stdout
   185  	cmd.Stderr = &h.stderr
   186  	cmd.Env = h.env
   187  	status := cmd.Run()
   188  	if *PrintLogs {
   189  		if h.stdout.Len() > 0 {
   190  			h.t.Log("standard output:")
   191  			h.t.Log(h.stdout.String())
   192  		}
   193  		if h.stderr.Len() > 0 {
   194  			h.t.Log("standard error:")
   195  			h.t.Log(h.stderr.String())
   196  		}
   197  	}
   198  	h.ran = true
   199  	return errors.Wrapf(status, "Error running %s\n%s", strings.Join(newargs, " "), h.stderr.String())
   200  }
   201  
   202  // Run runs the test go command, and expects it to succeed.
   203  func (h *Helper) Run(args ...string) {
   204  	if runtime.GOOS == "windows" {
   205  		mu.Lock()
   206  		defer mu.Unlock()
   207  	}
   208  	if status := h.DoRun(args); status != nil {
   209  		h.t.Logf("go %v failed unexpectedly: %v", args, status)
   210  		h.t.FailNow()
   211  	}
   212  }
   213  
   214  // runFail runs the test go command, and expects it to fail.
   215  func (h *Helper) runFail(args ...string) {
   216  	if status := h.DoRun(args); status == nil {
   217  		h.t.Fatalf("%+v", errors.New("testgo succeeded unexpectedly"))
   218  	} else {
   219  		h.t.Log("testgo failed as expected:", status)
   220  	}
   221  }
   222  
   223  // RunGo runs a go command, and expects it to succeed.
   224  func (h *Helper) RunGo(args ...string) {
   225  	cmd := exec.Command("go", args...)
   226  	h.stdout.Reset()
   227  	h.stderr.Reset()
   228  	cmd.Stdout = &h.stdout
   229  	cmd.Stderr = &h.stderr
   230  	cmd.Dir = h.wd
   231  	cmd.Env = h.env
   232  	status := cmd.Run()
   233  	if h.stdout.Len() > 0 {
   234  		h.t.Log("go standard output:")
   235  		h.t.Log(h.stdout.String())
   236  	}
   237  	if h.stderr.Len() > 0 {
   238  		h.t.Log("go standard error:")
   239  		h.t.Log(h.stderr.String())
   240  	}
   241  	if status != nil {
   242  		h.t.Logf("go %v failed unexpectedly: %v", args, status)
   243  		h.t.FailNow()
   244  	}
   245  }
   246  
   247  // NeedsExternalNetwork makes sure the tests needing external network will not
   248  // be run when executing tests in short mode.
   249  func NeedsExternalNetwork(t *testing.T) {
   250  	if testing.Short() {
   251  		t.Skip("skipping test: no external network in -short mode")
   252  	}
   253  }
   254  
   255  // NeedsGit will make sure the tests that require git will be skipped if the
   256  // git binary is not available.
   257  func NeedsGit(t *testing.T) {
   258  	if _, err := exec.LookPath("git"); err != nil {
   259  		t.Skip("skipping because git binary not found")
   260  	}
   261  }
   262  
   263  // RunGit runs a git command, and expects it to succeed.
   264  func (h *Helper) RunGit(dir string, args ...string) {
   265  	cmd := exec.Command("git", args...)
   266  	h.stdout.Reset()
   267  	h.stderr.Reset()
   268  	cmd.Stdout = &h.stdout
   269  	cmd.Stderr = &h.stderr
   270  	cmd.Dir = dir
   271  	cmd.Env = h.env
   272  	status := cmd.Run()
   273  	if *PrintLogs {
   274  		if h.stdout.Len() > 0 {
   275  			h.t.Logf("git %v standard output:", args)
   276  			h.t.Log(h.stdout.String())
   277  		}
   278  		if h.stderr.Len() > 0 {
   279  			h.t.Logf("git %v standard error:", args)
   280  			h.t.Log(h.stderr.String())
   281  		}
   282  	}
   283  	if status != nil {
   284  		h.t.Logf("git %v failed unexpectedly: %v", args, status)
   285  		h.t.FailNow()
   286  	}
   287  }
   288  
   289  // getStdout returns standard output of the testgo run as a string.
   290  func (h *Helper) getStdout() string {
   291  	if !h.ran {
   292  		h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
   293  	}
   294  	return h.stdout.String()
   295  }
   296  
   297  // getStderr returns standard error of the testgo run as a string.
   298  func (h *Helper) getStderr() string {
   299  	if !h.ran {
   300  		h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
   301  	}
   302  	return h.stderr.String()
   303  }
   304  
   305  // doGrepMatch looks for a regular expression in a buffer, and returns
   306  // whether it is found. The regular expression is matched against
   307  // each line separately, as with the grep command.
   308  func (h *Helper) doGrepMatch(match string, b *bytes.Buffer) bool {
   309  	if !h.ran {
   310  		h.t.Fatalf("%+v", errors.New("internal testsuite error: grep called before run"))
   311  	}
   312  	re := regexp.MustCompile(match)
   313  	for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
   314  		if re.Match(ln) {
   315  			return true
   316  		}
   317  	}
   318  	return false
   319  }
   320  
   321  // doGrep looks for a regular expression in a buffer and fails if it
   322  // is not found. The name argument is the name of the output we are
   323  // searching, "output" or "error".  The msg argument is logged on
   324  // failure.
   325  func (h *Helper) doGrep(match string, b *bytes.Buffer, name, msg string) {
   326  	if !h.doGrepMatch(match, b) {
   327  		h.t.Log(msg)
   328  		h.t.Logf("pattern %v not found in standard %s", match, name)
   329  		h.t.FailNow()
   330  	}
   331  }
   332  
   333  // grepStdout looks for a regular expression in the test run's
   334  // standard output and fails, logging msg, if it is not found.
   335  func (h *Helper) grepStdout(match, msg string) {
   336  	h.doGrep(match, &h.stdout, "output", msg)
   337  }
   338  
   339  // grepStderr looks for a regular expression in the test run's
   340  // standard error and fails, logging msg, if it is not found.
   341  func (h *Helper) grepStderr(match, msg string) {
   342  	h.doGrep(match, &h.stderr, "error", msg)
   343  }
   344  
   345  // grepBoth looks for a regular expression in the test run's standard
   346  // output or stand error and fails, logging msg, if it is not found.
   347  func (h *Helper) grepBoth(match, msg string) {
   348  	if !h.doGrepMatch(match, &h.stdout) && !h.doGrepMatch(match, &h.stderr) {
   349  		h.t.Log(msg)
   350  		h.t.Logf("pattern %v not found in standard output or standard error", match)
   351  		h.t.FailNow()
   352  	}
   353  }
   354  
   355  // doGrepNot looks for a regular expression in a buffer and fails if
   356  // it is found. The name and msg arguments are as for doGrep.
   357  func (h *Helper) doGrepNot(match string, b *bytes.Buffer, name, msg string) {
   358  	if h.doGrepMatch(match, b) {
   359  		h.t.Log(msg)
   360  		h.t.Logf("pattern %v found unexpectedly in standard %s", match, name)
   361  		h.t.FailNow()
   362  	}
   363  }
   364  
   365  // grepStdoutNot looks for a regular expression in the test run's
   366  // standard output and fails, logging msg, if it is found.
   367  func (h *Helper) grepStdoutNot(match, msg string) {
   368  	h.doGrepNot(match, &h.stdout, "output", msg)
   369  }
   370  
   371  // grepStderrNot looks for a regular expression in the test run's
   372  // standard error and fails, logging msg, if it is found.
   373  func (h *Helper) grepStderrNot(match, msg string) {
   374  	h.doGrepNot(match, &h.stderr, "error", msg)
   375  }
   376  
   377  // grepBothNot looks for a regular expression in the test run's
   378  // standard output or stand error and fails, logging msg, if it is
   379  // found.
   380  func (h *Helper) grepBothNot(match, msg string) {
   381  	if h.doGrepMatch(match, &h.stdout) || h.doGrepMatch(match, &h.stderr) {
   382  		h.t.Log(msg)
   383  		h.t.Fatalf("%+v", errors.Errorf("pattern %v found unexpectedly in standard output or standard error", match))
   384  	}
   385  }
   386  
   387  // doGrepCount counts the number of times a regexp is seen in a buffer.
   388  func (h *Helper) doGrepCount(match string, b *bytes.Buffer) int {
   389  	if !h.ran {
   390  		h.t.Fatalf("%+v", errors.New("internal testsuite error: doGrepCount called before run"))
   391  	}
   392  	re := regexp.MustCompile(match)
   393  	c := 0
   394  	for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
   395  		if re.Match(ln) {
   396  			c++
   397  		}
   398  	}
   399  	return c
   400  }
   401  
   402  // grepCountBoth returns the number of times a regexp is seen in both
   403  // standard output and standard error.
   404  func (h *Helper) grepCountBoth(match string) int {
   405  	return h.doGrepCount(match, &h.stdout) + h.doGrepCount(match, &h.stderr)
   406  }
   407  
   408  // creatingTemp records that the test plans to create a temporary file
   409  // or directory. If the file or directory exists already, it will be
   410  // removed. When the test completes, the file or directory will be
   411  // removed if it exists.
   412  func (h *Helper) creatingTemp(path string) {
   413  	if filepath.IsAbs(path) && !strings.HasPrefix(path, h.tempdir) {
   414  		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: creatingTemp(%q) with absolute path not in temporary directory", path))
   415  	}
   416  	// If we have changed the working directory, make sure we have
   417  	// an absolute path, because we are going to change directory
   418  	// back before we remove the temporary.
   419  	if h.wd != "" && !filepath.IsAbs(path) {
   420  		path = filepath.Join(h.pwd(), path)
   421  	}
   422  	h.Must(os.RemoveAll(path))
   423  	h.temps = append(h.temps, path)
   424  }
   425  
   426  // makeTempdir makes a temporary directory for a run of testgo. If
   427  // the temporary directory was already created, this does nothing.
   428  func (h *Helper) makeTempdir() {
   429  	if h.tempdir == "" {
   430  		var err error
   431  		h.tempdir, err = ioutil.TempDir("", "gotest")
   432  		h.Must(err)
   433  	}
   434  }
   435  
   436  // TempFile adds a temporary file for a run of testgo.
   437  func (h *Helper) TempFile(path, contents string) {
   438  	h.makeTempdir()
   439  	h.Must(os.MkdirAll(filepath.Join(h.tempdir, filepath.Dir(path)), 0755))
   440  	bytes := []byte(contents)
   441  	if strings.HasSuffix(path, ".go") {
   442  		formatted, err := format.Source(bytes)
   443  		if err == nil {
   444  			bytes = formatted
   445  		}
   446  	}
   447  	h.Must(ioutil.WriteFile(filepath.Join(h.tempdir, path), bytes, 0644))
   448  }
   449  
   450  // WriteTestFile writes a file to the testdata directory from memory.  src is
   451  // relative to ./testdata.
   452  func (h *Helper) WriteTestFile(src string, content string) error {
   453  	err := ioutil.WriteFile(filepath.Join(h.origWd, "testdata", src), []byte(content), 0666)
   454  	return err
   455  }
   456  
   457  // GetFile reads a file into memory
   458  func (h *Helper) GetFile(path string) io.ReadCloser {
   459  	content, err := os.Open(path)
   460  	if err != nil {
   461  		h.t.Fatalf("%+v", errors.Wrapf(err, "Unable to open file: %s", path))
   462  	}
   463  	return content
   464  }
   465  
   466  // GetTestFile reads a file from the testdata directory into memory.  src is
   467  // relative to ./testdata.
   468  func (h *Helper) GetTestFile(src string) io.ReadCloser {
   469  	fullPath := filepath.Join(h.origWd, "testdata", src)
   470  	return h.GetFile(fullPath)
   471  }
   472  
   473  // GetTestFileString reads a file from the testdata directory into memory.  src is
   474  // relative to ./testdata.
   475  func (h *Helper) GetTestFileString(src string) string {
   476  	srcf := h.GetTestFile(src)
   477  	defer srcf.Close()
   478  	content, err := ioutil.ReadAll(srcf)
   479  	if err != nil {
   480  		h.t.Fatalf("%+v", err)
   481  	}
   482  	return string(content)
   483  }
   484  
   485  // TempCopy copies a temporary file from testdata into the temporary directory.
   486  // dest is relative to the temp directory location, and src is relative to
   487  // ./testdata.
   488  func (h *Helper) TempCopy(dest, src string) {
   489  	in := h.GetTestFile(src)
   490  	defer in.Close()
   491  	h.TempDir(filepath.Dir(dest))
   492  	out, err := os.Create(filepath.Join(h.tempdir, dest))
   493  	if err != nil {
   494  		panic(err)
   495  	}
   496  	defer out.Close()
   497  	io.Copy(out, in)
   498  }
   499  
   500  // TempDir adds a temporary directory for a run of testgo.
   501  func (h *Helper) TempDir(path string) {
   502  	h.makeTempdir()
   503  	fullPath := filepath.Join(h.tempdir, path)
   504  	if err := os.MkdirAll(fullPath, 0755); err != nil && !os.IsExist(err) {
   505  		h.t.Fatalf("%+v", errors.Errorf("Unable to create temp directory: %s", fullPath))
   506  	}
   507  }
   508  
   509  // Path returns the absolute pathname to file with the temporary
   510  // directory.
   511  func (h *Helper) Path(name string) string {
   512  	if h.tempdir == "" {
   513  		h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: path(%q) with no tempdir", name))
   514  	}
   515  
   516  	var joined string
   517  	if name == "." {
   518  		joined = h.tempdir
   519  	} else {
   520  		joined = filepath.Join(h.tempdir, name)
   521  	}
   522  
   523  	// Ensure it's the absolute, symlink-less path we're returning
   524  	abs, err := filepath.EvalSymlinks(joined)
   525  	if err != nil {
   526  		h.t.Fatalf("%+v", errors.Wrapf(err, "internal testsuite error: could not get absolute path for dir(%q)", joined))
   527  	}
   528  	return abs
   529  }
   530  
   531  // MustExist fails if path does not exist.
   532  func (h *Helper) MustExist(path string) {
   533  	if err := h.ShouldExist(path); err != nil {
   534  		h.t.Fatalf("%+v", err)
   535  	}
   536  }
   537  
   538  // ShouldExist returns an error if path does not exist.
   539  func (h *Helper) ShouldExist(path string) error {
   540  	if !h.Exist(path) {
   541  		return errors.Errorf("%s does not exist but should", path)
   542  	}
   543  
   544  	return nil
   545  }
   546  
   547  // Exist returns whether or not a path exists
   548  func (h *Helper) Exist(path string) bool {
   549  	if _, err := os.Stat(path); err != nil {
   550  		if os.IsNotExist(err) {
   551  			return false
   552  		}
   553  		h.t.Fatalf("%+v", errors.Wrapf(err, "Error checking if path exists: %s", path))
   554  	}
   555  
   556  	return true
   557  }
   558  
   559  // MustNotExist fails if path exists.
   560  func (h *Helper) MustNotExist(path string) {
   561  	if err := h.ShouldNotExist(path); err != nil {
   562  		h.t.Fatalf("%+v", err)
   563  	}
   564  }
   565  
   566  // ShouldNotExist returns an error if path exists.
   567  func (h *Helper) ShouldNotExist(path string) error {
   568  	if h.Exist(path) {
   569  		return errors.Errorf("%s exists but should not", path)
   570  	}
   571  
   572  	return nil
   573  }
   574  
   575  // Cleanup cleans up a test that runs testgo.
   576  func (h *Helper) Cleanup() {
   577  	if h.wd != "" {
   578  		if err := os.Chdir(h.wd); err != nil {
   579  			// We are unlikely to be able to continue.
   580  			fmt.Fprintln(os.Stderr, "could not restore working directory, crashing:", err)
   581  			os.Exit(2)
   582  		}
   583  	}
   584  	// NOTE(mattn): It seems that sometimes git.exe is not dead
   585  	// when cleanup() is called. But we do not know any way to wait for it.
   586  	if runtime.GOOS == "windows" {
   587  		mu.Lock()
   588  		exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
   589  		mu.Unlock()
   590  	}
   591  	for _, path := range h.temps {
   592  		h.check(os.RemoveAll(path))
   593  	}
   594  	if h.tempdir != "" {
   595  		h.check(os.RemoveAll(h.tempdir))
   596  	}
   597  }
   598  
   599  // ReadManifest returns the manifest in the current directory.
   600  func (h *Helper) ReadManifest() string {
   601  	m := filepath.Join(h.pwd(), manifestName)
   602  	h.MustExist(m)
   603  
   604  	f, err := ioutil.ReadFile(m)
   605  	h.Must(err)
   606  	return string(f)
   607  }
   608  
   609  // ReadLock returns the lock in the current directory.
   610  func (h *Helper) ReadLock() string {
   611  	l := filepath.Join(h.pwd(), lockName)
   612  	h.MustExist(l)
   613  
   614  	f, err := ioutil.ReadFile(l)
   615  	h.Must(err)
   616  	return string(f)
   617  }
   618  
   619  // GetCommit treats repo as a path to a git repository and returns the current
   620  // revision.
   621  func (h *Helper) GetCommit(repo string) string {
   622  	repoPath := h.Path("pkg/dep/sources/https---" + strings.Replace(repo, "/", "-", -1))
   623  	cmd := exec.Command("git", "rev-parse", "HEAD")
   624  	cmd.Dir = repoPath
   625  	out, err := cmd.CombinedOutput()
   626  	if err != nil {
   627  		h.t.Fatalf("%+v", errors.Wrapf(err, "git commit failed: out -> %s", string(out)))
   628  	}
   629  	return strings.TrimSpace(string(out))
   630  }