howl.bundle
after_each ->
  _G.bundles = {}
  bundle.dirs = {}
with_bundle_dir = (name, f) ->
  with_tmpdir (dir) ->
    b_dir = dir / name
    b_dir\mkdir!
    status, err = pcall f, b_dir
    error(err) unless status
    mod_name = name\lower!\gsub '[%s%p]+', '_'
    pcall(bundle.unload, mod_name) if _G.bundles[mod_name]
bundle_init = (info = {}, spec = {}) ->
  ret = ''
  ret ..= "#{spec.code}\n" if spec.code
  mod = author: 'bundle_spec', description: 'spec_bundle', license: 'MIT'
  mod[k] = v for k,v in pairs info
  ret ..= 'return { info = {'
  ret ..= table.concat [k .. '="' .. v .. '"' for k,v in pairs mod], ','
  ret ..= '}, '
  if spec.other_returns
    ret ..= table.concat [k .. '="' .. tostring(v) .. '"' for k,v in pairs spec.other_returns], ','
  if spec.unload
    ret ..= "unload = #{spec.unload} }"
  else
    ret ..= 'unload = function() end }'
  ret
.unloaded holds the adjusted names of any unloaded bundles
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  for name in *{'foo-bar', 'frob_nic'}
    b_dir = dir / name
    b_dir\mkdir!
    b_dir\join('init.lua').contents = bundle_init :name
  assert.same { 'foo_bar', 'frob_nic' }, bundle.unloaded
  bundle.load_by_name 'foo_bar'
  assert.same { 'frob_nic' }, bundle.unloaded
  bundle.unload 'foo_bar'
  assert.same { 'foo_bar', 'frob_nic' }, bundle.unloaded
load_from_dir(dir)
raises an error if dir is not a directory
assert.raises 'directory', -> bundle.load_from_dir File '/not-a-directory'
raises an error if the bundle init file is missing or incomplete
with_tmpdir (dir) ->
  assert.raises 'find file', -> bundle.load_from_dir dir
  init = dir / 'init.lua'
  init\touch!
  assert.raises 'Incorrect bundle', -> bundle.load_from_dir dir
  init.contents = 'return {}'
  assert.raises 'info missing', -> bundle.load_from_dir dir
  init.contents = 'return { info = {} }'
  assert.raises 'missing info field', -> bundle.load_from_dir dir
assigns the returned bundle table to bundles using the dir basename
mod = author: 'bundle_spec', description: 'spec_bundle', license: 'MIT'
with_bundle_dir 'foo', (dir) ->
  dir\join('init.lua').contents = bundle_init mod
  bundle.load_from_dir dir
  assert.same _G.bundles.foo.info, mod
  assert.is_equal 'function', type _G.bundles.foo.unload
massages the assigned module name to fit with naming standards if necessary
with_bundle_dir 'Test-hello 2', (dir) ->
  dir\join('init.lua').contents = bundle_init!
  bundle.load_from_dir dir
  assert.not_nil _G.bundles.test_hello_2
does nothing if the bundle is already loaded
with_bundle_dir 'two_times', (dir) ->
  dir\join('init.lua').contents = bundle_init!
  bundle.load_from_dir dir
  bundle.load_from_dir dir
raises an error upon implicit global writes
with_tmpdir (dir) ->
  dir\join('init.lua').contents = [[
    file = bundle_file('bundle_aux.lua')
    return {
      info = {
        author = 'spec',
        description = 'desc',
        license = 'MIT',
      },
      file = file
    }
  ]]
  assert.raises 'implicit global', -> bundle.load_from_dir dir
(exposed bundle helpers)
bundle_file provides access to bundle files
with_bundle_dir 'test', (dir) ->
  dir\join('init.lua').contents = [[
    local file = bundle_file('bundle_aux.lua')
    return {
      info = {
        author = 'spec',
        description = 'desc',
        license = 'MIT',
      },
      unload = function() end,
      file = file
    }
  ]]
  bundle.load_from_dir dir
  assert.equal _G.bundles.test.file, dir / 'bundle_aux.lua'
provide_module(name, prefix = nil)
makes a sub directory available for loading globally with require
with_bundle_dir 'test', (dir) ->
  mod = dir\join('testmod')
  mod\mkdir_p!
  mod\join('init.moon').contents = '{root: true}'
  mod\join('other.moon').contents = '{other: true}'
  dir\join('init.lua').contents = bundle_init nil, {
    code: 'provide_module("testmod")'
  }
  bundle.load_from_dir dir
  assert.same {root: true}, require 'testmod'
  assert.same {other: true}, require 'testmod.other'
 
require_bundle
ensures the required bundle is loaded before the dependent one
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  first_dir = dir\join('first')
  first_dir\mkdir!
  second_dir = dir\join('second')
  second_dir\mkdir!
  third_dir = dir\join('third')
  third_dir\mkdir!
  first_dir\join('init.lua').contents = bundle_init!
  third_dir\join('init.lua').contents = bundle_init!
  second_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("third")\nrequire_bundle("first")'
  }
  bundle.load_from_dir second_dir
  assert.is_not_nil _G.bundles.first
  assert.is_not_nil _G.bundles.third
detects cyclic dependencies
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  first_dir = dir\join('first')
  first_dir\mkdir!
  second_dir = dir\join('second')
  second_dir\mkdir!
  first_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("second")'
  }
  second_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("first")'
  }
  assert.raises 'Cyclic dependency', ->
    bundle.load_from_dir second_dir
 
 
 
load_all()
loads all found bundles in all directories in bundle.dirs
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  for name in *{'foo', 'bar'}
    b_dir = dir / name
    b_dir\mkdir!
    b_dir\join('init.lua').contents = bundle_init :name
  bundle.load_all!
  assert.not_nil _G.bundles.foo
  assert.not_nil _G.bundles.bar
skips any hidden entries
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / '.hidden'
  b_dir\mkdir!
  b_dir\join('init.lua').contents = bundle_init name: 'hidden'
  bundle.load_all!
  assert.same [name for name, _ in pairs _G.bundles], {}
raises an error if bundle names conflict
with_tmpdir (dir) ->
  for name in *{'foo', 'bar'}
    b_dir = dir / name / 'my_bundle'
    b_dir\mkdir_p!
    b_dir\join('init.lua').contents = bundle_init :name
  bundle.dirs = {dir\join('foo'), dir\join('bar')}
  assert.raises 'conflict', -> bundle.load_all!
 
load_by_name(name)
loads the bundle with the specified name, if not already loaded
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / 'named'
  b_dir\mkdir!
  b_dir\join('init.lua').contents = bundle_init name: 'named'
  bundle.load_by_name 'named'
  assert.not_nil _G.bundles.named
  bundle.load_by_name 'named'
raises an error if the bundle could not be found
assert.raises 'not found', -> bundle.load_by_name 'oh_bundle_where_art_thouh'
 
unload(name)
raises an error if no bundle with the given name exists
assert.raises 'not found', -> bundle.unload 'serenity'
(for an existing bundle)
mod = name: 'bunny'
calls the bundle unload function and removes the bundle from _G.bundles
with_bundle_dir 'bunny', (dir) ->
  dir\join('init.lua').contents = bundle_init mod, unload: 'function() _G.bunny_bundle_unload = true end'
  bundle.load_from_dir dir
  bundle.unload 'bunny'
  assert.is_true _G.bunny_bundle_unload
  assert.is_nil _G.bundles.bunny
returns early with an error if the unload function raises an error
with_bundle_dir 'bad_seed', (dir) ->
  dir\join('init.lua').contents = bundle_init mod, unload: 'function() error("barf!") end'
  bundle.load_from_dir dir
  assert.raises 'barf!', -> bundle.unload 'bad_seed'
  assert.is_not_nil _G.bundles.bad_seed
with_bundle_dir 'dash-love', (dir) ->
  dir\join('init.lua').contents = bundle_init name: 'dash-love'
  bundle.load_from_dir dir
  assert.no_error -> bundle.unload 'dash-love'
removes any references to provided modules
with_bundle_dir 'test', (dir) ->
  mod = dir\join('testmod')
  mod\mkdir_p!
  mod\join('init.moon').contents = '{root: true}'
  mod\join('other.moon').contents = '{other: true}'
  dir\join('init.lua').contents = bundle_init nil, {
    code: 'provide_module("testmod")'
  }
  bundle.load_from_dir dir
  assert.same {root: true}, require 'testmod'
  assert.same {other: true}, require 'testmod.other'
  bundle.unload 'test'
  assert.is_false pcall require, 'testmod'
  assert.is_false pcall require, 'testmod.other'
 
 
from_file(file)
returns the adjusted name of the containing bundle if any
with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / 'my-bundle'
  init = b_dir\join('init.lua')
  assert.equal 'my_bundle', bundle.from_file(init)
  assert.is_nil bundle.from_file(File('/bin/ls'))
  assert.is_nil bundle.from_file(dir\join('directly_under_root'))