Class | Gem::TestCase |
In: |
lib/rubygems/test_case.rb
|
Parent: | MiniTest::Unit::TestCase |
RubyGemTestCase provides a variety of methods for testing rubygems and gem-related behavior in a sandbox. Through RubyGemTestCase you can install and uninstall gems, fetch remote gems through a stub fetcher and be assured your normal set of gems is not affected.
Tests are always run at a safe level of 1.
Returns the make command for the current platform. For versions of Ruby built on MS Windows with VC++ or Borland it will return ‘nmake’. On all other platforms, including Cygwin, it will return ‘make’.
# File lib/rubygems/test_case.rb, line 748 748: def self.make_command 749: ENV["make"] || (vc_windows? ? 'nmake' : 'make') 750: end
Allows tests to use a random (but controlled) port number instead of a hardcoded one. This helps CI tools when running parallels builds on the same builder slave.
# File lib/rubygems/test_case.rb, line 773 773: def self.process_based_port 774: @@process_based_port ||= 8000 + $$ % 1000 775: end
Finds the path to the ruby executable
# File lib/rubygems/test_case.rb, line 805 805: def self.rubybin 806: ruby = ENV["RUBY"] 807: return ruby if ruby 808: ruby = "ruby" 809: rubyexe = "#{ruby}.exe" 810: 811: 3.times do 812: if File.exist? ruby and File.executable? ruby and !File.directory? ruby 813: return File.expand_path(ruby) 814: end 815: if File.exist? rubyexe and File.executable? rubyexe 816: return File.expand_path(rubyexe) 817: end 818: ruby = File.join("..", ruby) 819: end 820: 821: begin 822: require "rbconfig" 823: File.join(RbConfig::CONFIG["bindir"], 824: RbConfig::CONFIG["ruby_install_name"] + 825: RbConfig::CONFIG["EXEEXT"]) 826: rescue LoadError 827: "ruby" 828: end 829: end
Returns whether or not we‘re on a version of Ruby built with VC++ (or Borland) versus Cygwin, Mingw, etc.
# File lib/rubygems/test_case.rb, line 731 731: def self.vc_windows? 732: RUBY_PLATFORM.match('mswin') 733: end
Is this test being run on a Windows platform?
# File lib/rubygems/test_case.rb, line 716 716: def self.win_platform? 717: Gem.win_platform? 718: end
# File lib/rubygems/test_case.rb, line 328 328: def all_spec_names 329: Gem::Specification.map(&:full_name) 330: end
TODO: move to minitest
# File lib/rubygems/test_case.rb, line 84 84: def assert_path_exists path, msg = nil 85: msg = message(msg) { "Expected path '#{path}' to exist" } 86: assert File.exist?(path), msg 87: end
Allows the proper version of rake to be used for the test.
# File lib/rubygems/test_case.rb, line 787 787: def build_rake_in 788: gem_ruby = Gem.ruby 789: Gem.ruby = @@ruby 790: env_rake = ENV["rake"] 791: ENV["rake"] = @@rake 792: yield @@rake 793: ensure 794: Gem.ruby = gem_ruby 795: if env_rake 796: ENV["rake"] = env_rake 797: else 798: ENV.delete("rake") 799: end 800: end
creates a temporary directory with hax
# File lib/rubygems/test_case.rb, line 279 279: def create_tmpdir 280: tmpdir = nil 281: Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp 282: tmpdir = File.join tmpdir, "test_rubygems_#{$$}" 283: FileUtils.mkdir_p tmpdir 284: return tmpdir 285: end
Construct a new Gem::Dependency.
# File lib/rubygems/test_case.rb, line 845 845: def dep name, *requirements 846: Gem::Dependency.new name, *requirements 847: end
Builds and installs the Gem::Specification spec
# File lib/rubygems/test_case.rb, line 246 246: def install_gem spec, options = {} 247: require 'rubygems/installer' 248: 249: use_ui Gem::MockGemUi.new do 250: Dir.chdir @tempdir do 251: Gem::Builder.new(spec).build 252: end 253: end 254: 255: gem = File.join(@tempdir, File.basename(spec.cache_file)).untaint 256: 257: Gem::Installer.new(gem, options.merge({:wrappers => true})).install 258: end
Builds and installs the Gem::Specification spec into the user dir
# File lib/rubygems/test_case.rb, line 263 263: def install_gem_user spec 264: install_gem spec, :user_install => true 265: end
Install the provided specs
# File lib/rubygems/test_case.rb, line 432 432: def install_specs(*specs) 433: Gem::Specification.add_specs(*specs) 434: Gem.searcher = nil 435: end
Returns the make command for the current platform. For versions of Ruby built on MS Windows with VC++ or Borland it will return ‘nmake’. On all other platforms, including Cygwin, it will return ‘make’.
# File lib/rubygems/test_case.rb, line 757 757: def make_command 758: ENV["make"] || (vc_windows? ? 'nmake' : 'make') 759: end
Enables pretty-print for all tests
# File lib/rubygems/test_case.rb, line 290 290: def mu_pp(obj) 291: s = '' 292: s = PP.pp obj, s 293: s = s.force_encoding(Encoding.default_external) if defined? Encoding 294: s.chomp 295: end
Create a new spec (or gem if passed an array of files) and set it up properly. Use this instead of util_spec and util_gem.
# File lib/rubygems/test_case.rb, line 441 441: def new_spec name, version, deps = nil, *files 442: require 'rubygems/specification' 443: 444: spec = Gem::Specification.new do |s| 445: s.platform = Gem::Platform::RUBY 446: s.name = name 447: s.version = version 448: s.author = 'A User' 449: s.email = 'example@example.com' 450: s.homepage = 'http://example.com' 451: s.summary = "this is a summary" 452: s.description = "This is a test description" 453: 454: Array(deps).each do |n, req| 455: s.add_dependency n, (req || '>= 0') 456: end 457: 458: s.files.push(*files) unless files.empty? 459: 460: yield s if block_given? 461: end 462: 463: spec.loaded_from = spec.spec_file 464: 465: unless files.empty? then 466: write_file spec.spec_file do |io| 467: io.write spec.to_ruby_for_cache 468: end 469: 470: util_build_gem spec 471: 472: cache_file = File.join @tempdir, 'gems', "#{spec.full_name}.gem" 473: FileUtils.mkdir_p File.dirname cache_file 474: FileUtils.mv spec.cache_file, cache_file 475: FileUtils.rm spec.spec_file 476: end 477: 478: spec 479: end
Returns whether or not the nmake command could be found.
# File lib/rubygems/test_case.rb, line 764 764: def nmake_found? 765: system('nmake /? 1>NUL 2>&1') 766: end
See ::process_based_port
# File lib/rubygems/test_case.rb, line 780 780: def process_based_port 781: self.class.process_based_port 782: end
Creates a Gem::Specification with a minimum of extra work. name and version are the gem‘s name and version, platform, author, email, homepage, summary and description are defaulted. The specification is yielded for customization.
The gem is added to the installed gems in +@gemhome+ and to the current source_index.
Use this with write_file to build an installed gem.
# File lib/rubygems/test_case.rb, line 343 343: def quick_gem(name, version='2') 344: require 'rubygems/specification' 345: 346: spec = Gem::Specification.new do |s| 347: s.platform = Gem::Platform::RUBY 348: s.name = name 349: s.version = version 350: s.author = 'A User' 351: s.email = 'example@example.com' 352: s.homepage = 'http://example.com' 353: s.summary = "this is a summary" 354: s.description = "This is a test description" 355: 356: yield(s) if block_given? 357: end 358: 359: Gem::Specification.map # HACK: force specs to (re-)load before we write 360: 361: written_path = write_file spec.spec_file do |io| 362: io.write spec.to_ruby_for_cache 363: end 364: 365: spec.loaded_from = spec.loaded_from = written_path 366: 367: Gem::Specification.add_spec spec.for_cache 368: 369: return spec 370: end
# File lib/rubygems/test_case.rb, line 372 372: def quick_spec name, version = '2' 373: # TODO: deprecate 374: require 'rubygems/specification' 375: 376: spec = Gem::Specification.new do |s| 377: s.platform = Gem::Platform::RUBY 378: s.name = name 379: s.version = version 380: s.author = 'A User' 381: s.email = 'example@example.com' 382: s.homepage = 'http://example.com' 383: s.summary = "this is a summary" 384: s.description = "This is a test description" 385: 386: yield(s) if block_given? 387: end 388: 389: spec.loaded_from = spec.spec_file 390: 391: Gem::Specification.add_spec spec 392: 393: return spec 394: end
Reads a binary file at path
# File lib/rubygems/test_case.rb, line 309 309: def read_binary(path) 310: Gem.read_binary path 311: end
Reads a Marshal file at path
# File lib/rubygems/test_case.rb, line 300 300: def read_cache(path) 301: open path.dup.untaint, 'rb' do |io| 302: Marshal.load io.read 303: end 304: end
TODO: move to minitest
# File lib/rubygems/test_case.rb, line 90 90: def refute_path_exists path, msg = nil 91: msg = message(msg) { "Expected path '#{path}' to not exist" } 92: refute File.exist?(path), msg 93: end
Constructs a new Gem::Requirement.
# File lib/rubygems/test_case.rb, line 852 852: def req *requirements 853: return requirements.first if Gem::Requirement === requirements.first 854: Gem::Requirement.create requirements 855: end
setup prepares a sandboxed location to install gems. All installs are directed to a temporary directory. All install plugins are removed.
If the RUBY environment variable is set the given path is used for Gem::ruby. The local platform is set to i386-mswin32 for Windows or i686-darwin8.10.1 otherwise.
If the KEEP_FILES environment variable is set the files will not be removed from /tmp/test_rubygems_#{$$}.#{Time.now.to_i}.
# File lib/rubygems/test_case.rb, line 113 113: def setup 114: super 115: 116: @orig_gem_home = ENV['GEM_HOME'] 117: @orig_gem_path = ENV['GEM_PATH'] 118: 119: @current_dir = Dir.pwd 120: @ui = Gem::MockGemUi.new 121: 122: tmpdir = nil 123: Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp 124: 125: if ENV['KEEP_FILES'] then 126: @tempdir = File.join(tmpdir, "test_rubygems_#{$$}.#{Time.now.to_i}") 127: else 128: @tempdir = File.join(tmpdir, "test_rubygems_#{$$}") 129: end 130: @tempdir.untaint 131: @gemhome = File.join @tempdir, 'gemhome' 132: @userhome = File.join @tempdir, 'userhome' 133: 134: @orig_ruby = if ruby = ENV['RUBY'] then 135: Gem.class_eval { ruby, @ruby = @ruby, ruby } 136: ruby 137: end 138: 139: Gem.ensure_gem_subdirectories @gemhome 140: 141: @orig_LOAD_PATH = $LOAD_PATH.dup 142: $LOAD_PATH.map! { |s| File.expand_path s } 143: 144: Dir.chdir @tempdir 145: 146: @orig_ENV_HOME = ENV['HOME'] 147: ENV['HOME'] = @userhome 148: Gem.instance_variable_set :@user_home, nil 149: 150: FileUtils.mkdir_p @gemhome 151: FileUtils.mkdir_p @userhome 152: 153: Gem.use_paths(@gemhome) 154: 155: Gem.loaded_specs.clear 156: Gem.unresolved_deps.clear 157: 158: Gem.configuration.verbose = true 159: Gem.configuration.update_sources = true 160: 161: @gem_repo = "http://gems.example.com/" 162: @uri = URI.parse @gem_repo 163: Gem.sources.replace [@gem_repo] 164: 165: Gem.searcher = nil 166: Gem::SpecFetcher.fetcher = nil 167: 168: @orig_BASERUBY = Gem::ConfigMap[:BASERUBY] 169: Gem::ConfigMap[:BASERUBY] = Gem::ConfigMap[:ruby_install_name] 170: 171: @orig_arch = Gem::ConfigMap[:arch] 172: 173: if win_platform? 174: util_set_arch 'i386-mswin32' 175: else 176: util_set_arch 'i686-darwin8.10.1' 177: end 178: 179: @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" 180: 181: # TODO: move to installer test cases 182: Gem.post_build_hooks.clear 183: Gem.post_install_hooks.clear 184: Gem.post_uninstall_hooks.clear 185: Gem.pre_install_hooks.clear 186: Gem.pre_uninstall_hooks.clear 187: 188: # TODO: move to installer test cases 189: Gem.post_build do |installer| 190: @post_build_hook_arg = installer 191: true 192: end 193: 194: Gem.post_install do |installer| 195: @post_install_hook_arg = installer 196: end 197: 198: Gem.post_uninstall do |uninstaller| 199: @post_uninstall_hook_arg = uninstaller 200: end 201: 202: Gem.pre_install do |installer| 203: @pre_install_hook_arg = installer 204: true 205: end 206: 207: Gem.pre_uninstall do |uninstaller| 208: @pre_uninstall_hook_arg = uninstaller 209: end 210: end
Constructs a new Gem::Specification.
# File lib/rubygems/test_case.rb, line 860 860: def spec name, version, &block 861: Gem::Specification.new name, v(version), &block 862: end
teardown restores the process to its original state and removes the tempdir unless the KEEP_FILES environment variable was set.
# File lib/rubygems/test_case.rb, line 216 216: def teardown 217: $LOAD_PATH.replace @orig_LOAD_PATH 218: 219: Gem::ConfigMap[:BASERUBY] = @orig_BASERUBY 220: Gem::ConfigMap[:arch] = @orig_arch 221: 222: if defined? Gem::RemoteFetcher then 223: Gem::RemoteFetcher.fetcher = nil 224: end 225: 226: Dir.chdir @current_dir 227: 228: FileUtils.rm_rf @tempdir unless ENV['KEEP_FILES'] 229: 230: ENV['GEM_HOME'] = @orig_gem_home 231: ENV['GEM_PATH'] = @orig_gem_path 232: 233: _ = @orig_ruby 234: Gem.class_eval { @ruby = _ } if _ 235: 236: if @orig_ENV_HOME then 237: ENV['HOME'] = @orig_ENV_HOME 238: else 239: ENV.delete 'HOME' 240: end 241: end
Uninstalls the Gem::Specification spec
# File lib/rubygems/test_case.rb, line 269 269: def uninstall_gem spec 270: require 'rubygems/uninstaller' 271: 272: Gem::Uninstaller.new(spec.name, 273: :executables => true, :user_install => true).uninstall 274: end
Builds a gem from spec and places it in File.join @gemhome, ‘cache‘. Automatically creates files based on +spec.files+
# File lib/rubygems/test_case.rb, line 400 400: def util_build_gem(spec) 401: dir = spec.gem_dir 402: FileUtils.mkdir_p dir 403: 404: Dir.chdir dir do 405: spec.files.each do |file| 406: next if File.exist? file 407: FileUtils.mkdir_p File.dirname(file) 408: File.open file, 'w' do |fp| fp.puts "# #{file}" end 409: end 410: 411: use_ui Gem::MockGemUi.new do 412: Gem::Builder.new(spec).build 413: end 414: 415: cache = spec.cache_file 416: FileUtils.mv File.basename(cache), cache 417: end 418: end
Removes all installed gems from +@gemhome+.
# File lib/rubygems/test_case.rb, line 423 423: def util_clear_gems 424: FileUtils.rm_rf File.join(@gemhome, "gems") # TODO: use Gem::Dirs 425: FileUtils.rm_rf File.join(@gemhome, "specifications") 426: Gem::Specification.reset 427: end
Creates a gem with name, version and deps. The specification will be yielded before gem creation for customization. The gem will be placed in File.join @tempdir, ‘gems‘. The specification and .gem file location are returned.
# File lib/rubygems/test_case.rb, line 508 508: def util_gem(name, version, deps = nil, &block) 509: # TODO: deprecate 510: raise "deps or block, not both" if deps and block 511: 512: if deps then 513: block = proc do |s| 514: # Since Hash#each is unordered in 1.8, sort 515: # the keys and iterate that way so the tests are 516: # deteriminstic on all implementations. 517: deps.keys.sort.each do |n| 518: s.add_dependency n, (deps[n] || '>= 0') 519: end 520: end 521: end 522: 523: spec = quick_gem(name, version, &block) 524: 525: util_build_gem spec 526: 527: cache_file = File.join @tempdir, 'gems', "#{spec.original_name}.gem" 528: FileUtils.mkdir_p File.dirname cache_file 529: FileUtils.mv spec.cache_file, cache_file 530: FileUtils.rm spec.spec_file 531: 532: spec.loaded_from = nil 533: 534: [spec, cache_file] 535: end
Gzips data.
# File lib/rubygems/test_case.rb, line 540 540: def util_gzip(data) 541: out = StringIO.new 542: 543: Zlib::GzipWriter.wrap out do |io| 544: io.write data 545: end 546: 547: out.string 548: end
Creates several default gems which all have a lib/code.rb file. The gems are not installed but are available in the cache dir.
+@a1+: | gem a version 1, this is the best-described gem. |
+@a2+: | gem a version 2 |
+@a3a: | gem a version 3.a |
+@a_evil9+: | gem a_evil version 9, use this to ensure similarly-named gems don‘t collide with a. |
+@b2+: | gem b version 2 |
+@c1_2+: | gem c version 1.2 |
+@pl1+: | gem pl version 1, this gem has a legacy platform of i386-linux. |
Additional prerelease gems may also be created:
+@a2_pre+: | gem a version 2.a |
TODO: nuke this and fix tests. this should speed up a lot
# File lib/rubygems/test_case.rb, line 568 568: def util_make_gems(prerelease = false) 569: @a1 = quick_gem 'a', '1' do |s| 570: s.files = %w[lib/code.rb] 571: s.require_paths = %w[lib] 572: s.date = Gem::Specification::TODAY - 86400 573: s.homepage = 'http://a.example.com' 574: s.email = %w[example@example.com example2@example.com] 575: s.authors = %w[Example Example2] 576: s.description = "This line is really, really long. So long, in fact, that it is more than eighty characters long! The purpose of this line is for testing wrapping behavior because sometimes people don't wrap their text to eighty characters. Without the wrapping, the text might not look good in the RSS feed.\n\nAlso, a list:\n* An entry that\\'s actually kind of sort\n* an entry that\\'s really long, which will probably get wrapped funny. That's ok, somebody wasn't thinking straight when they made it more than eighty characters.\n" 577: end 578: 579: init = proc do |s| 580: s.files = %w[lib/code.rb] 581: s.require_paths = %w[lib] 582: end 583: 584: @a2 = quick_gem('a', '2', &init) 585: @a3a = quick_gem('a', '3.a', &init) 586: @a_evil9 = quick_gem('a_evil', '9', &init) 587: @b2 = quick_gem('b', '2', &init) 588: @c1_2 = quick_gem('c', '1.2', &init) 589: 590: @pl1 = quick_gem 'pl', '1' do |s| # l for legacy 591: s.files = %w[lib/code.rb] 592: s.require_paths = %w[lib] 593: s.platform = Gem::Platform.new 'i386-linux' 594: s.instance_variable_set :@original_platform, 'i386-linux' 595: end 596: 597: if prerelease 598: @a2_pre = quick_gem('a', '2.a', &init) 599: write_file File.join(*??[gems #{@a2_pre.original_name} lib code.rb]) 600: util_build_gem @a2_pre 601: end 602: 603: write_file File.join(*??[gems #{@a1.original_name} lib code.rb]) 604: write_file File.join(*??[gems #{@a2.original_name} lib code.rb]) 605: write_file File.join(*??[gems #{@a3a.original_name} lib code.rb]) 606: write_file File.join(*??[gems #{@b2.original_name} lib code.rb]) 607: write_file File.join(*??[gems #{@c1_2.original_name} lib code.rb]) 608: write_file File.join(*??[gems #{@pl1.original_name} lib code.rb]) 609: 610: [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2, @pl1].each do |spec| 611: util_build_gem spec 612: end 613: 614: FileUtils.rm_r File.join(@gemhome, "gems", @pl1.original_name) 615: end
Set the platform to arch
# File lib/rubygems/test_case.rb, line 627 627: def util_set_arch(arch) 628: Gem::ConfigMap[:arch] = arch 629: platform = Gem::Platform.new arch 630: 631: Gem.instance_variable_set :@platforms, nil 632: Gem::Platform.instance_variable_set :@local, nil 633: 634: platform 635: end
Sets up a fake fetcher using the gems from util_make_gems. Optionally additional prerelease gems may be included.
Gems created by this method may be fetched using Gem::RemoteFetcher.
# File lib/rubygems/test_case.rb, line 643 643: def util_setup_fake_fetcher(prerelease = false) 644: require 'zlib' 645: require 'socket' 646: require 'rubygems/remote_fetcher' 647: 648: @fetcher = Gem::FakeFetcher.new 649: 650: util_make_gems(prerelease) 651: Gem::Specification.reset 652: 653: @all_gems = [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2].sort 654: @all_gem_names = @all_gems.map { |gem| gem.full_name } 655: 656: gem_names = [@a1.full_name, @a2.full_name, @a3a.full_name, @b2.full_name] 657: @gem_names = gem_names.sort.join("\n") 658: 659: Gem::RemoteFetcher.fetcher = @fetcher 660: end
Sets up Gem::SpecFetcher to return information from the gems in specs. Best used with +@all_gems+ from util_setup_fake_fetcher.
# File lib/rubygems/test_case.rb, line 666 666: def util_setup_spec_fetcher(*specs) 667: specs -= Gem::Specification._all 668: Gem::Specification.add_specs(*specs) 669: 670: spec_fetcher = Gem::SpecFetcher.fetcher 671: 672: prerelease, _ = Gem::Specification.partition { |spec| 673: spec.version.prerelease? 674: } 675: 676: spec_fetcher.specs[@uri] = [] 677: Gem::Specification.each do |spec| 678: spec_tuple = [spec.name, spec.version, spec.original_platform] 679: spec_fetcher.specs[@uri] << spec_tuple 680: end 681: 682: spec_fetcher.latest_specs[@uri] = [] 683: Gem::Specification.latest_specs.each do |spec| 684: spec_tuple = [spec.name, spec.version, spec.original_platform] 685: spec_fetcher.latest_specs[@uri] << spec_tuple 686: end 687: 688: spec_fetcher.prerelease_specs[@uri] = [] 689: prerelease.each do |spec| 690: spec_tuple = [spec.name, spec.version, spec.original_platform] 691: spec_fetcher.prerelease_specs[@uri] << spec_tuple 692: end 693: 694: v = Gem.marshal_version 695: 696: Gem::Specification.each do |spec| 697: path = "#{@gem_repo}quick/Marshal.#{v}/#{spec.original_name}.gemspec.rz" 698: data = Marshal.dump spec 699: data_deflate = Zlib::Deflate.deflate data 700: @fetcher.data[path] = data_deflate 701: end unless Gem::RemoteFetcher === @fetcher # HACK for test_download_to_cache 702: 703: nil # force errors 704: end
Creates a spec with name, version and deps.
# File lib/rubygems/test_case.rb, line 484 484: def util_spec(name, version, deps = nil, &block) 485: # TODO: deprecate 486: raise "deps or block, not both" if deps and block 487: 488: if deps then 489: block = proc do |s| 490: # Since Hash#each is unordered in 1.8, sort 491: # the keys and iterate that way so the tests are 492: # deteriminstic on all implementations. 493: deps.keys.sort.each do |n| 494: s.add_dependency n, (deps[n] || '>= 0') 495: end 496: end 497: end 498: 499: quick_spec(name, version, &block) 500: end
Deflates data
# File lib/rubygems/test_case.rb, line 709 709: def util_zip(data) 710: Zlib::Deflate.deflate data 711: end
Construct a new Gem::Version.
# File lib/rubygems/test_case.rb, line 867 867: def v string 868: Gem::Version.create string 869: end
Returns whether or not we‘re on a version of Ruby built with VC++ (or Borland) versus Cygwin, Mingw, etc.
# File lib/rubygems/test_case.rb, line 739 739: def vc_windows? 740: RUBY_PLATFORM.match('mswin') 741: end
Is this test being run on a Windows platform?
# File lib/rubygems/test_case.rb, line 723 723: def win_platform? 724: Gem.win_platform? 725: end
Writes a binary file to path which is relative to +@gemhome+
# File lib/rubygems/test_case.rb, line 316 316: def write_file(path) 317: path = File.join @gemhome, path unless Pathname.new(path).absolute? 318: dir = File.dirname path 319: FileUtils.mkdir_p dir 320: 321: open path, 'wb' do |io| 322: yield io if block_given? 323: end 324: 325: path 326: end