xolvio / qualityfaster

An example project showing how to create robust and maintainable acceptance tests
262 stars 58 forks source link

Meteor 1.5 problem with duplicated method and collection names #72

Open suezzo opened 6 years ago

suezzo commented 6 years ago

Hello, recently I've started a new project using Meteor 1.5. At the begging, I had a problem with dynamic import. I updated my wallaby config with help of #71 issue. It's pretty similar to my previous config running fine on 1.4.4. This time I get an error about duplicated methods and collection names. When I change collection name when Wallaby is already running the error is gone. I'll attach my config below. Thank you in advance for any help.

// How to run:
//
// 1. Start your Meteor app and wait until it has built.
// 2. Start Wallaby with this configuration.

// How to modify for your project:
//
// 1. Change relativeMeteorAppPath (line 16 and 69) to the relative location of your Meteor app.
// 2. Adjust the files array (line 37) to match all your Meteor app server files.
// 3. Adjust the tests array (line 44) to match all your server tests that should run with Wallaby.

var path = require('path')
var nodePath = require('child_process')
  .execSync('meteor node -e "process.stdout.write(process.execPath)"', { encoding: 'utf8' })
var relativeMeteorAppPath = ''

module.exports = function (wallaby) {
  process.env.NODE_PATH += path.delimiter +
    path.join(
      wallaby.localProjectDir,
      relativeMeteorAppPath,
      '.meteor/local/build/programs/server/node_modules'
    )

  process.env.NODE_PATH += path.delimiter +
    path.join(
      wallaby.localProjectDir,
      relativeMeteorAppPath,
      'node_modules'
    )

  // process.env.NODE_PATH += path.delimiter +
  //   path.join(wallaby.projectCacheDir, relativeMeteorAppPath, 'imports');

  return {
    files: [
      { pattern: relativeMeteorAppPath + '/tests/helpers.js', instrument: false },
      { pattern: relativeMeteorAppPath + '/imports/api/**/*.js', load: false },
      { pattern: relativeMeteorAppPath + '/imports/api/**/*-test.js', ignore: true },
      { pattern: relativeMeteorAppPath + '/.meteor/**/*', ignore: true },
      { pattern: relativeMeteorAppPath + '/utility/meteor/*.js', ignore: false }
    ],

    tests: [
      { pattern: relativeMeteorAppPath + '/imports/api/**/*-test.js' }
    ],

    compilers: {
      '**/*.js': wallaby.compilers.babel({
        presets: ['es2015', 'stage-1']
      })
    },

    env: {
      type: 'node',
      runner: nodePath
    },

    testFramework: 'mocha',

    workers: {
      recycle: true,
      initial: 1,
      regular: 1
    },

    debug: true,

    bootstrap: function (wallaby) {
      var relativeMeteorAppPath = ''

      wallaby.delayStart()

      if (!process.env.ROOT_URL) {
        process.env.ROOT_URL = 'http://localhost:3000/'
      }
      if (!process.env.MONGO_URL) {
        process.env.MONGO_URL = 'mongodb://127.0.0.1:3001/wallaby-server'
      }

      process.on('unhandledRejection', function (reason, promise) {
        var exception = reason.stack ? reason.stack.replace(/\(\/.*?\/instrumented\//g, '(') : reason
        console.error('Unhandled promise rejection', exception)
      })

      var path = require('path')
      var appPath = path.resolve(wallaby.localProjectDir, relativeMeteorAppPath)
      var serverPath = path.resolve(appPath, '.meteor/local/build/programs/server')
      var meteorModulesPath = path.resolve(appPath, '.meteor/local/dev_bundle/lib/node_modules')
      var meteorServerModulesPath = path.resolve(appPath, '.meteor/local/dev_bundle/server-lib/node_modules')
      process.argv.splice(2, 0, 'program.json')
      try {
        process.chdir(serverPath)
      } catch (error) {
        if (error.message.match(/^ENOENT/)) {
          throw new Error('You need to run the Meteor app before you start Wallaby!')
        } else {
          throw error
        }
      }

      var Fiber = require('fibers')

      require('babel-polyfill')
      require(path.join(meteorModulesPath, 'reify/lib/runtime'))
        .enable(module.constructor.prototype)
      // This should allow Fibers to work in across an await point in an async function but doesn't seem to work
      require(path.join(meteorServerModulesPath, 'meteor-promise'))
        .makeCompatible(Promise, Fiber)

      var fs = require('fs')
      var Future = require('fibers/future')
      var _ = require('underscore')
      var sourcemap_support = require('source-map-support')

      // var bootUtils = require('./boot-utils.js');
      var files = require(path.resolve(serverPath, './mini-files.js'))
      var npmRequire = require(path.resolve(serverPath, './npm-require.js')).require
      var Profile = require(path.resolve(serverPath, './profile.js')).Profile

      // This code is duplicated in tools/main.js.
      var MIN_NODE_VERSION = 'v0.10.41'

      var hasOwn = Object.prototype.hasOwnProperty

      if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
        process.stderr.write(
          'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n')
        process.exit(1)
      }

      // read our control files
      var serverJsonPath = path.resolve(process.argv[2])
      var serverDir = path.dirname(serverJsonPath)
      var serverJson = require(path.resolve(serverPath, './server-json.js'))
      var configJson =
        JSON.parse(fs.readFileSync(path.resolve(serverDir, 'config.json'), 'utf8'))

      // Set up environment
      __meteor_bootstrap__ = {
        startupHooks: [],
        serverDir: serverDir,
        configJson: configJson }
      __meteor_runtime_config__ = { meteorRelease: configJson.meteorRelease }

      if (!process.env.APP_ID) {
        process.env.APP_ID = configJson.appId
      }

      // Map from load path to its source map.
      var parsedSourceMaps = {}

      // Read all the source maps into memory once.
      _.each(serverJson.load, function (fileInfo) {
        if (fileInfo.sourceMap) {
          var rawSourceMap = fs.readFileSync(
            path.resolve(serverDir, fileInfo.sourceMap), 'utf8')
          // Parse the source map only once, not each time it's needed. Also remove
          // the anti-XSSI header if it's there.
          var parsedSourceMap = JSON.parse(rawSourceMap.replace(/^\)\]\}'/, ''))
          // source-map-support doesn't ever look at the sourcesContent field, so
          // there's no point in keeping it in memory.
          delete parsedSourceMap.sourcesContent
          var url
          if (fileInfo.sourceMapRoot) {
            // Add the specified root to any root that may be in the file.
            parsedSourceMap.sourceRoot = path.join(
              fileInfo.sourceMapRoot, parsedSourceMap.sourceRoot || '')
          }
          parsedSourceMaps[path.resolve(__dirname, fileInfo.path)] = parsedSourceMap
        }
      })

      var retrieveSourceMap = function (pathForSourceMap) {
        if (_.has(parsedSourceMaps, pathForSourceMap)) { return { map: parsedSourceMaps[pathForSourceMap] } }
        return null
      }

      var origWrapper = sourcemap_support.wrapCallSite
      var wrapCallSite = function (frame) {
        var frame = origWrapper(frame)
        var wrapGetter = function (name) {
          var origGetter = frame[name]
          frame[name] = function (arg) {
            // replace a custom location domain that we set for better UX in Chrome
            // DevTools (separate domain group) in source maps.
            var source = origGetter(arg)
            if (! source) { return source }
            return source.replace(/(^|\()meteor:\/\/..app\//, '$1')
          }
        }
        wrapGetter('getScriptNameOrSourceURL')
        wrapGetter('getEvalOrigin')

        return frame
      }
      sourcemap_support.install({
        // Use the source maps specified in program.json instead of parsing source
        // code for them.
        retrieveSourceMap: retrieveSourceMap,
        // For now, don't fix the source line in uncaught exceptions, because we
        // haven't fixed handleUncaughtExceptions in source-map-support to properly
        // locate the source files.
        handleUncaughtExceptions: false,
        wrapCallSite: wrapCallSite
      })

      var specialArgPaths = {
        'packages/modules-runtime.js': function () {
          return {
            npmRequire: npmRequire,
            Profile: Profile
          }
        },

        'packages/dynamic-import.js': function (file) {
          var dynamicImportInfo = {}

          Object.keys(configJson.clientPaths).map(function (key) {
            var programJsonPath = path.resolve(configJson.clientPaths[key])
            var programJson = require(programJsonPath)

            dynamicImportInfo[key] = {
              dynamicRoot: path.join(path.dirname(programJsonPath), 'dynamic')
            }
          })

          dynamicImportInfo.server = {
            dynamicRoot: path.join(serverDir, 'dynamic')
          }

          return { dynamicImportInfo: dynamicImportInfo }
        }
      }

      const mocha = require('mocha')
      const suite = (wallaby) ? wallaby.testFramework.suite : mocha.suite

      function fiberize (fn) {
        return function (done) {
          var self = this

          Fiber(function () {
            try {
              if (fn.length == 1) {
                fn.call(self, done)
              } else {
                fn.call(self)
                done()
              }
            } catch (e) {
              process.nextTick(function () {
                throw e
              })
            }
          }).run()
        }
      }

      suite.on('pre-require', (context) => {
        ['beforeEach', 'afterEach', 'after', 'before', 'it'].forEach((method) => {
          const original = global[method]

          context[method] = _.wrap(original, function (fn) {
            const args = Array.prototype.slice.call(arguments, 1)
            if (_.isFunction(_.last(args))) {
              args.push(fiberize(args.pop()))
            }
            return fn.apply(this, args)
          })

          _.extend(context[method], _(original).pick('only', 'skip'))
        })
      })

      Fiber(function () {
        _.each(serverJson.load, function (fileInfo) {
          var code = fs.readFileSync(path.resolve(serverDir, fileInfo.path))
          var nonLocalNodeModulesPaths = []

          function addNodeModulesPath (path) {
            nonLocalNodeModulesPaths.push(
              files.pathResolve(serverDir, path)
            )
          }

          if (typeof fileInfo.node_modules === 'string') {
            addNodeModulesPath(fileInfo.node_modules)
          } else if (fileInfo.node_modules) {
            _.each(fileInfo.node_modules, function (info, path) {
              if (! info.local) {
                addNodeModulesPath(path)
              }
            })
          }

          function statOrNull (path) {
            try {
              return fs.statSync(path)
            } catch (e) {
              return null
            }
          }

          var Npm = {
            /**
             * @summary Require a package that was specified using
             * `Npm.depends()`.
             * @param  {String} name The name of the package to require.
             * @locus Server
             * @memberOf Npm
             */
            require: Profile(function getBucketName (name) {
              return 'Npm.require(' + JSON.stringify(name) + ')'
            }, function (name) {
              if (nonLocalNodeModulesPaths.length === 0) {
                return require(name)
              }

              var fullPath

              nonLocalNodeModulesPaths.some(function (nodeModuleBase) {
                var packageBase = files.convertToOSPath(files.pathResolve(
                  nodeModuleBase,
                  name.split('/', 1)[0]
                ))

                if (statOrNull(packageBase)) {
                  return fullPath = files.convertToOSPath(
                    files.pathResolve(nodeModuleBase, name)
                  )
                }
              })

              if (fullPath) {
                return require(fullPath)
              }

              try {
                return require(name)
              } catch (e) {
                // Try to guess the package name so we can print a nice
                // error message
                // fileInfo.path is a standard path, use files.pathSep
                var filePathParts = fileInfo.path.split(files.pathSep)
                var packageName = filePathParts[1].replace(/\.js$/, '')

                // XXX better message
                throw new Error(
                  "Can't find npm module '" + name +
                    "'. Did you forget to call 'Npm.depends' in package.js " +
                    "within the '" + packageName + "' package?")
              }
            })
          }
          var getAsset = function (assetPath, encoding, callback) {
            var fut
            if (! callback) {
              fut = new Future()
              callback = fut.resolver()
            }
            // This assumes that we've already loaded the meteor package, so meteor
            // itself can't call Assets.get*. (We could change this function so that
            // it doesn't call bindEnvironment if you don't pass a callback if we need
            // to.)
            var _callback = Package.meteor.Meteor.bindEnvironment(function (err, result) {
              if (result && ! encoding)
              // Sadly, this copies in Node 0.10.
              { result = new Uint8Array(result) }
              callback(err, result)
            }, function (e) {
              console.log('Exception in callback of getAsset', e.stack)
            })

            // Convert a DOS-style path to Unix-style in case the application code was
            // written on Windows.
            assetPath = files.convertToStandardPath(assetPath)

            // Unicode normalize the asset path to prevent string mismatches when
            // using this string elsewhere.
            assetPath = files.unicodeNormalizePath(assetPath)

            if (!fileInfo.assets || !_.has(fileInfo.assets, assetPath)) {
              _callback(new Error('Unknown asset: ' + assetPath))
            } else {
              var filePath = path.join(serverDir, fileInfo.assets[assetPath])
              fs.readFile(files.convertToOSPath(filePath), encoding, _callback)
            }
            if (fut) { return fut.wait() }
          }

          var Assets = {
            getText: function (assetPath, callback) {
              return getAsset(assetPath, 'utf8', callback)
            },
            getBinary: function (assetPath, callback) {
              return getAsset(assetPath, undefined, callback)
            },
            /**
             * @summary Get the absolute path to the static server asset. Note that assets are read-only.
             * @locus Server [Not in build plugins]
             * @memberOf Assets
             * @param {String} assetPath The path of the asset, relative to the application's `private` subdirectory.
             */
            absoluteFilePath: function (assetPath) {
              // Unicode normalize the asset path to prevent string mismatches when
              // using this string elsewhere.
              assetPath = files.unicodeNormalizePath(assetPath)

              if (!fileInfo.assets || !_.has(fileInfo.assets, assetPath)) {
                throw new Error('Unknown asset: ' + assetPath)
              }

              assetPath = files.convertToStandardPath(assetPath)
              var filePath = path.join(serverDir, fileInfo.assets[assetPath])
              return files.convertToOSPath(filePath)
            }
          }

          var wrapParts = ['(function(Npm,Assets']

          var specialArgs =
            hasOwn.call(specialArgPaths, fileInfo.path) &&
            specialArgPaths[fileInfo.path](fileInfo)

          var specialKeys = Object.keys(specialArgs || {})
          specialKeys.forEach(function (key) {
            wrapParts.push(',' + key)
          })

          // \n is necessary in case final line is a //-comment
          wrapParts.push('){', code, '\n})')
          var wrapped = wrapParts.join('')

          // It is safer to use the absolute path when source map is present as
          // different tooling, such as node-inspector, can get confused on relative
          // urls.

          // fileInfo.path is a standard path, convert it to OS path to join with
          // __dirname
          var fileInfoOSPath = files.convertToOSPath(fileInfo.path)
          var absoluteFilePath = path.resolve(__dirname, fileInfoOSPath)

          var scriptPath =
            parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfoOSPath
          // The final 'true' is an undocumented argument to runIn[Foo]Context that
          // causes it to print out a descriptive error message on parse error. It's
          // what require() uses to generate its errors.
          var func = require('vm').runInThisContext(wrapped, scriptPath, true)
          var args = [Npm, Assets]

          specialKeys.forEach(function (key) {
            args.push(specialArgs[key])
          })

          func.apply(global, args)
        })

        process.chdir(appPath)
        wallaby.start()
      }).run()
    }
  }
}