{"action":"created","comment":{"url":"https://api.github.com/repos/aces/cbrain/pulls/comments/70617703","id":70617703,"diff_hunk":"@@ -0,0 +1,389 @@\n+#\n+# Bourreau-side tests for the Boutiques framework, using a mock tool (boutiquesTestApp.rb).\n+# Tests the generated cluster task by using it to create cluster commands that test against\n+# the mock tool and its expected output for each set of input parameters.\n+#\n+\n+# Required files for inclusion\n+require 'rails_helper'\n+require 'spec_helper'\n+require_relative 'test_helpers'\n+\n+# Add helper methods for performing tests\n+include TestHelpers\n+\n+# Testing the Boutiques framework on the Bourreau side\n+describe \"Bourreau Boutiques Tests\" do\n+\n+  # Run before block to create required input files\n+  before(:all) do\n+    FileUtils.touch('c')  # For -C\n+    FileUtils.touch('f')  # For -d\n+    FileUtils.touch('jf') # For -j\n+    [1,2].each { |i| FileUtils.touch(\"f#{i}\") } # For -f\n+  end\n+\n+  # Post-test cleanup via after block\n+  after(:all) do\n+    # Delete the input and output files when they exist\n+    ['c','f','jf','f1','f2'].each{ |f| File.delete(f) if File.exist?(f) }\n+    PotenOutFiles.each { |f| File.delete(f) if File.exist?(f) }\n+  end\n+\n+  context \"Cbrain external\" do\n+    # Run before block to create ClusterTask object\n+    before(:each) do\n+      # Get the schema and json descriptor\n+      schema         = SchemaTaskGenerator.default_schema\n+      descriptor     = File.join(__dir__, TestScriptDescriptor)\n+      # Generate a new task class via the Boutiques framework and integrate it into cbrain\n+      @boutiquesTask = SchemaTaskGenerator.generate(schema, descriptor)\n+      @boutiquesTask.integrate if File.exists?(descriptor)\n+      # Create a new instance of the generated task class\n+      @task          = CbrainTask::BoutiquesTest.new\n+      @task.params   = {}\n+      # Give access to the generated task class itself\n+      @task_const    = \"CbrainTask::#{SchemaTaskGenerator.classify(@task.name)}\".constantize\n+    end\n+\n+    # Tests expected behaviour of the auto-generated cluster task\n+    describe \"Generated ClusterTask Object\" do\n+\n+      # Check necessary properties of the descriptor object\n+      describe \"descriptor\" do\n+        it \"has the right name\" do\n+          expect( @boutiquesTask.descriptor['name'] ).to eq( 'BoutiquesTest' )\n+        end\n+      end\n+\n+      # Test some basic properties\n+      it \"creates a tool\" do\n+        expect( Tool.exists?( :cbrain_task_class => @task_const.to_s ) ).to be true\n+      end\n+\n+      it \"makes the tool accessible\" do\n+        expect( @task_const.tool ).not_to be_nil\n+      end\n+\n+      # Test that the apply_template method works as expected\n+      # for some representative (or previously buggy) cases\n+      describe \"apply_template\" do\n+\n+        # Define some default parameters to use in the tests\n+        before(:each) do\n+          @template = 'cmd [1] [2] [3] [4] [5]'\n+          @def_keys = {'[1]' => 'one.txt', '[2]' => 2,             '[3]' => 't.csv', '[4]' => nil, '[5]' => nil}\n+          @flags    = {'[1]' => '-1',      '[2]' => '--long-flag', '[3]' => '-t',    '[4]' => nil, '[5]' => nil}\n+          @seps     = {'[1]' => '=',       '[2]' => '~',           '[3]' => ' ',     '[4]' => nil, '[5]' => nil}\n+        end\n+\n+        it \"handles string substitutions\" do\n+          s = @task.apply_template(@template, @def_keys)\n+          expect( s.strip ).to eq( 'cmd one.txt 2 t.csv' )\n+        end\n+\n+        it \"handles string substituions with spaces\" do\n+          s = @task.apply_template(@template, @def_keys.merge({'[4]' => '4 4'}))\n+          expect( s.strip ).to eq( \"cmd one.txt 2 t.csv '4 4'\" )\n+        end\n+\n+        # Characters special to the shell or ruby's gsub should not interfere\n+        # In this case, ensure that ' is escaped properly\n+        it \"handles special meaning characters\" do\n+          s = @task.apply_template(@template, @def_keys.merge({'[4]' => \" '; arg\"}))\n+          expect( s.strip ).to eq( \"cmd one.txt 2 t.csv ' '\\\\''; arg'\" )\n+        end\n+\n+        it \"handles substitutions with command-line flags\" do\n+          s = @task.apply_template(@template, @def_keys, flags: @flags)\n+          expect( s.strip ).to eq( \"cmd -1 one.txt --long-flag 2 -t t.csv\" )\n+        end\n+\n+        it \"handles substitution with flag-type inputs\" do\n+          s = @task.apply_template(@template, @def_keys.merge({'[4]' => true}), flags: {'[4]' => '-f'})\n+          expect( s.strip ).to eq( \"cmd one.txt 2 t.csv -f\" )\n+        end\n+\n+        it \"handles special flag separator substitution\" do\n+          s = @task.apply_template(@template,\n+                @def_keys.merge({'[4]' => true}),\n+                flags:      @flags.merge({'[4]' => '-f'}),\n+                separators: @seps)\n+          expect( s.strip ).to eq( \"cmd -1=one.txt --long-flag~2 -t t.csv -f\" )\n+        end\n+\n+        it \"properly strips endings\" do\n+          s = @task.apply_template(@template,\n+                @def_keys.merge({'[4]' => true, '[5]' => '9.tex'}),\n+                flags:      @flags.merge({'[4]' => '-f', '[5]' => '-tex'}),\n+                separators: @seps,\n+                strip:      [ '.txt', '.tex' ])\n+          expect( s.strip ).to eq( \"cmd -1=one --long-flag~2 -t t.csv -f -tex 9\" )\n+        end\n+\n+      end\n+\n+      # Test that creating a basic cluster command in isolation works\n+      it \"can create cluster commands\" do\n+        @task.params[:A] = \"A_VAL\"\n+        expect( @task.cluster_commands[0].strip ).to eq( './' + TestScriptName + ' -A A_VAL' )\n+      end\n+\n+    end\n+\n+    # Testing Boutiques via the mock 'submission' of a local script, using the output of cluster_commands\n+    context 'Cluster Command Generation with Mock Program' do\n+\n+      # After each local test, destroy the output files, so they don't interfere with downstream tests\n+      after(:each) do\n+        destroyOutputFiles\n+      end\n+\n+      # Perform tests by running the cmd line given by cluster_commands and checking the exit code\n+      BasicTests.each do |test|\n+        # Testing for unrecognized inputs will not work here, since apply_template will ignore them\n+        next if test[0].include?( \"unrecognized\" )\n+        # The apply_template method adds the separator on its own, so we need only check that works\n+        next if test[0].include?( \"fails when special separator\" )\n+        # Run the test\n+        it \"#{test[0]}\" do\n+          # Convert string argument to params dict\n+          @task.params = ArgumentDictionary.( test[1] )\n+          # Run the generated command line from cluster_commands\n+          exit_code = runTestScript( @task.cluster_commands[0].gsub('./'+TestScriptName,''), test[3] || [] )\n+          # Check that the exit code is appropriate\n+          expect( exit_code ).to eq( test[2] )\n+        end\n+      end\n+\n+    end\n+  end\n+\n+  # Tests requiring complete Cbrain Bourreau-side system functionality\n+  describe 'Cbrain Internal' do\n+\n+    # Test that the generated Boutiques task can handle output file saving and renaming\n+    # Mostly tests the autogenerated save_results method of the mock Boutiques task\n+    context 'Output File Handling' do\n+\n+      # Define some useful constants (constants get redefined in before(:each) blocks)\n+      before(:all) do\n+        # The group, provider, and user ids used downstream\n+        GID, UID, DPID = Group.everyone.id, User.admin.id, 9\n+        # The warning message used when unable to find optional output files\n+        OptOutFileNotFoundWarning = \"Unable to find optional output file: \"\n+      end\n+\n+      # Note: FactoryGirl methods should be used instead, but only if their db changes get rolled back in the before(:each) block\n+      # This is currently not happening, possibly due to some versioning issues\n+      before(:each) do\n+        # Destroy any pre-existing userfiles\n+        Userfile.all.each{ |uf| uf.destroy }\n+        # Create a mock task required output file\n+        @fname = DefReqOutName\n+        FileUtils.touch(@fname)\n+        # Helper method for getting filetype classes\n+        ftype = lambda { |fname| Userfile.suggested_file_type(fname) || SingleFile }\n+        @userfileClass = ftype.(@fname)\n+        # Get the schema and json descriptor\n+        descriptor     = File.join(__dir__, TestScriptDescriptor)\n+        # Generate a new task class via the Boutiques framework and integrate it into cbrain\n+        @boutiquesTask = SchemaTaskGenerator.generate(SchemaTaskGenerator.default_schema, descriptor)\n+        @boutiquesTask.integrate\n+        # Instantiate an object of the new class type\n+        @task          = CbrainTask::BoutiquesTest.new\n+        # Destroy any prior existing data providers (so we use a clean, lone one)\n+        DataProvider.all.each { |dp| dp.destroy }\n+        # Create a local data_provider to hold our files\n+        @provider      = FlatDirLocalDataProvider.new({ :online => true, :read_only => false, :remote_dir => '.' })\n+        @provider.id, @provider.name = DPID, 'test_provider'\n+        @provider.user_id, @provider.group_id = UID, GID\n+        @provider.save!\n+        # Add a local input file to the data provider (allows smarter lookup in userfile_exists)\n+        @file_c, @ft   = 'c', ftype.(@file_c)\n+        newFile = @task.safe_userfile_find_or_new(@ft, name: @file_c, data_provider_id: DPID, user_id: UID, group_id: GID)\n+        newFile.save!\n+        # Fill in data necessary for the task to check for and save the output file '@fname'\n+        @task.params   = {:ro => @fname, :interface_userfile_ids => [Userfile.all.first.id]}\n+        @task.user_id, @task.group_id = UID, GID\n+        # Generate a simulated exit file, as if the task had run\n+        @simExitFile   = @task.exit_cluster_filename\n+        FileUtils.touch( @simExitFile )\n+        # The basic properties for the required output file\n+        @reqOutfileProps = {:name => @fname, :data_provider_id => @provider.id}\n+        # Optional output file properties\n+        @optOutFileName  = OptOutName\n+        @optFileClass    = ftype.( @optOutFileName )\n+      end\n+\n+      # Clean up after each test\n+      after(:each) do\n+        # Delete any generated output files\n+        destroyOutputFiles\n+        # Also need to get rid of the (simulated) exit file\n+        fename = '.qsub.exit.BoutiquesTest.-1'\n+        File.delete( fename ) if File.exists?( fename )\n+        # Destroy the registered userfiles and the data_provider, so as not to affect downstream tests\n+        # Necessary for tests that create and/or register userfiles in the it block\n+        Userfile.all.each{ |uf| uf.destroy }\n+        DataProvider.all.each { |dp| dp.destroy }\n+      end\n+\n+      # Check that the input file could be properly registered\n+      it \"has access to the registered input file\" do\n+        expect( @task.userfile_exists( @ft, {:name => @file_c, :data_provider_id => @provider.id}) ).to be true\n+      end\n+\n+      # Check that save_results works as expected for existent files\n+      it \"can save results files\" do\n+        # Make sure the file on the filesystem exists\n+        expect( File.exists? @fname ).to be true\n+        # Ensure the file has not been registered/created yet\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be false\n+        # Ensure that saving the results occurs error-free\n+        expect( @task.save_results ).to be true\n+        # Ensure that the file now exists in the data_provider\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be true\n+      end\n+\n+      # Check that, when a required output file is not present, the task fails gracefully\n+      it \"fails non-catastrophically when a required output file is not there\" do\n+        # The output file should exist\n+        expect( File.exists? @fname ).to be true\n+        # Destroy the required output file\n+        File.delete( @fname )\n+        # Attempting to save_results should return a 'failure' error code\n+        expect( @task.save_results ).to be false\n+      end\n+\n+      # Check that optional output files are saved appropriately\n+      it \"handles optional output files when present\" do\n+        # Create the optional output file\n+        FileUtils.touch( @optOutFileName )\n+        # Inform the generated task to look for the optional output file\n+        @task.params = @task.params.merge({:oo => @optOutFileName})\n+        # Ensure the file exists\n+        expect( File.exists? @optOutFileName ).to be true\n+        # Attempt to save both the optional and required output files. Should succeed.\n+        expect( @task.save_results ).to be true\n+        # Both files should exist in the data_provider\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be true\n+        expect( @task.userfile_exists(@optFileClass,  {:name => @optOutFileName, :data_provider_id => @provider.id}) ).to be true\n+        # The log should not have warned about being unable to find the optional output file\n+        expect( @task.getlog.include?( OptOutFileNotFoundWarning + @optOutFileName ) ).to be false\n+      end\n+\n+      # Check that there is no error when specified optional output files are not found\n+      # However, a logging should occur to note that fact\n+      it \"handles absent optional output files properly\" do\n+        # Inform the generated task to look for the optional output file\n+        @task.params = @task.params.merge({:oo => @optOutFileName})\n+        # Ensure the file does not exist\n+        expect( File.exists? @optOutFileName ).to be false\n+        # Attempt to save both the optional and required output files. Should succeed (in terms of return value).\n+        expect( @task.save_results ).to be true\n+        # Only the required file should exist in the data_provider\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be true\n+        expect( @task.userfile_exists(@optFileClass,  {:name => @optOutFileName, :data_provider_id => @provider.id}) ).to be false\n+        # Check that a notice was logged to warn of the missing optional output file\n+        expect( @task.getlog.include?( OptOutFileNotFoundWarning + @optOutFileName ) ).to be true\n+      end\n+\n+      # Ensure the files do not survive between tests\n+      it 'should destroy results files between tests' do\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be false\n+      end\n+\n+      # This test ensures that when a naming collision occurs, the second file is renamed appropriately\n+      it \"renames results files\" do\n+        # First ensure the file exists on the system\n+        expect( File.exists? @fname ).to be true\n+        # However, the file must not already exist in the data_provider\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be false\n+        # Saving the output files via the generated task should occur error-free\n+        expect( @task.save_results ).to be true\n+        # The file should now exist in the data_provider\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be true\n+        # Create a new task and have it save results as well (with the same name for the output file)\n+        task2        = CbrainTask::BoutiquesTest.new\n+        task2.params = {:ro => @fname, :interface_userfile_ids => [Userfile.all.first.id]}\n+        task2.user_id, task2.group_id = UID, GID\n+        # Check that there are no files that match the renamed filename pattern\n+        renamedFileRegex = /^#{@fname.split(\".\")[0]}-.*-\\d{9}\\.txt$/\n+        expect( Userfile.all.any? { |f| (f.name.to_s =~ renamedFileRegex) == 0 } ).to be false\n+        # Saving the second output file should be error free\n+        expect( task2.save_results ).to be true\n+        # Initial file should still be there with the correct, original name\n+        expect( @task.userfile_exists(@userfileClass, @reqOutfileProps) ).to be true\n+        expect( Userfile.where( :name => @fname ).first.nil? ).not_to be true\n+        # New file should exist and have been renamed appropriately\n+        expect( Userfile.all.one? { |f| (f.name.to_s =~ renamedFileRegex) == 0 } ).to be true\n+      end\n+\n+    end # End output file handling tests\n+\n+    # Test that a Tool Config is created iff both the bourreau and the descriptor specify docker\n+    context 'Default ToolConfig creation' do\n+\n+      before(:each) do\n+        # Ensure the Bourreau does not have docker installed by default\n+        resource = RemoteResource.current_resource\n+        resource.docker_present = false\n+        resource.save!\n+        # Get the schema and json descriptor\n+        schema           = SchemaTaskGenerator.default_schema\n+        @descriptor      = File.join(__dir__, TestScriptDescriptor)\n+        # Generate a new task class via the Boutiques framework, without integrating it\n+        @boutiquesTask   = SchemaTaskGenerator.generate(schema, @descriptor)\n+        @boutiquesTask.descriptor[\"docker-image\"] = nil # tool does not use docker by default\n+        @task_const_name = \"CbrainTask::#{SchemaTaskGenerator.classify(@boutiquesTask.name)}\"\n+        # Destroy any tools/toolconfigs for the tool, if any exist\n+        ToolConfig.where(tool_id: CbrainTask::BoutiquesTest.tool.id).destroy_all rescue nil\n+        Tool.where(:cbrain_task_class => @task_const_name).destroy_all rescue nil\n+      end\n+\n+      after(:each) do\n+        # Destroy any tools/toolconfigs for the tool, if any exist\n+        ToolConfig.where(tool_id: @task_const_name.constantize.tool.id).destroy_all\n+        Tool.where(:cbrain_task_class => @task_const_name).destroy_all\n+        # Ensure the Bourreau does not have docker installed by default\n+        resource = RemoteResource.current_resource\n+        resource.docker_present = false\n+        resource.save!\n+      end\n+\n+      it \"is not done with only docker in task\" do\n+        @boutiquesTask.descriptor['docker-image'] = 'placeholder_string'\n+        @boutiquesTask.integrate if File.exists?(@descriptor)\n+        expect( ToolConfig.exists?( :tool_id => @task_const_name.constantize.tool.id ) ).to be false\n+      end\n+\n+      it \"is not done with only docker in bourreau\" do","path":"Bourreau/spec/boutiques/boutiques_tester_spec.rb","position":361,"original_position":361,"commit_id":"2a306c5f4e134e487f2957b2ba02e20494b224b9","original_commit_id":"2a306c5f4e134e487f2957b2ba02e20494b224b9","user":{"login":"glatard","id":5174953,"avatar_url":"https://avatars.githubusercontent.com/u/5174953?v=3","gravatar_id":"","url":"https://api.github.com/users/glatard","html_url":"https://github.com/glatard","followers_url":"https://api.github.com/users/glatard/followers","following_url":"https://api.github.com/users/glatard/following{/other_user}","gists_url":"https://api.github.com/users/glatard/gists{/gist_id}","starred_url":"https://api.github.com/users/glatard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/glatard/subscriptions","organizations_url":"https://api.github.com/users/glatard/orgs","repos_url":"https://api.github.com/users/glatard/repos","events_url":"https://api.github.com/users/glatard/events{/privacy}","received_events_url":"https://api.github.com/users/glatard/received_events","type":"User","site_admin":false},"body":"you mean \"tool config is not created when descriptor has no docker image\"?","created_at":"2016-07-13T12:46:50Z","updated_at":"2016-07-13T12:46:50Z","html_url":"https://github.com/aces/cbrain/pull/420#discussion_r70617703","pull_request_url":"https://api.github.com/repos/aces/cbrain/pulls/420","_links":{"self":{"href":"https://api.github.com/repos/aces/cbrain/pulls/comments/70617703"},"html":{"href":"https://github.com/aces/cbrain/pull/420#discussion_r70617703"},"pull_request":{"href":"https://api.github.com/repos/aces/cbrain/pulls/420"}}},"pull_request":{"url":"https://api.github.com/repos/aces/cbrain/pulls/420","id":75664229,"html_url":"https://github.com/aces/cbrain/pull/420","diff_url":"https://github.com/aces/cbrain/pull/420.diff","patch_url":"https://github.com/aces/cbrain/pull/420.patch","issue_url":"https://api.github.com/repos/aces/cbrain/issues/420","number":420,"state":"open","locked":false,"title":"Boutiques test suite: initial version","user":{"login":"ttaa9","id":6237441,"avatar_url":"https://avatars.githubusercontent.com/u/6237441?v=3","gravatar_id":"","url":"https://api.github.com/users/ttaa9","html_url":"https://github.com/ttaa9","followers_url":"https://api.github.com/users/ttaa9/followers","following_url":"https://api.github.com/users/ttaa9/following{/other_user}","gists_url":"https://api.github.com/users/ttaa9/gists{/gist_id}","starred_url":"https://api.github.com/users/ttaa9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ttaa9/subscriptions","organizations_url":"https://api.github.com/users/ttaa9/orgs","repos_url":"https://api.github.com/users/ttaa9/repos","events_url":"https://api.github.com/users/ttaa9/events{/privacy}","received_events_url":"https://api.github.com/users/ttaa9/received_events","type":"User","site_admin":false},"body":"For #382. \r\nIncludes:\r\n- boutiquesTestApp.rb: a mock application with as many Boutiques features as possible\r\n- descriptor_test.json: the Boutiques descriptor for the mock program\r\n- boutiques_tester_spec.rb: the rspec file containing the tests. There are two: one for the Bourreau side and one for the BrainPortal side.\r\n- test_helpers.rb: contains helper methods for the tests on both sides (including a compact form of many of the tests)\r\n\r\nSome issues:\r\n- The automatic transaction rollback does not seem to work consistently on the Bourreau side, so right now it is handled hackily\r\n- I have pasted the Boutiques ruby json validator into the tests folder. That is probably not where it belongs, but since it is nowhere else in cbrain (that I know of), I have put it there for now. It does not do anything special right now, but I suspect we will be adding to it in the near future with extra checks. When that happens it should be used to validate all the json descriptors at runtime (instead of just the internal validation). It is also unfortunate that it will be in two separate repos, but then again so is the main Boutiques schema. Let me know what you think of this; I can remove it fairly easily if needed.\r\n- I'm sure there are tests for things that I missed","created_at":"2016-06-29T15:00:09Z","updated_at":"2016-07-13T12:46:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8d7b9dd91adb19a29aa1206010eeedb72f0d4f0f","assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/aces/cbrain/milestones/8","html_url":"https://github.com/aces/cbrain/milestones/4.5.0","labels_url":"https://api.github.com/repos/aces/cbrain/milestones/8/labels","id":1799507,"number":8,"title":"4.5.0","description":"","creator":{"login":"natacha-beck","id":777590,"avatar_url":"https://avatars.githubusercontent.com/u/777590?v=3","gravatar_id":"","url":"https://api.github.com/users/natacha-beck","html_url":"https://github.com/natacha-beck","followers_url":"https://api.github.com/users/natacha-beck/followers","following_url":"https://api.github.com/users/natacha-beck/following{/other_user}","gists_url":"https://api.github.com/users/natacha-beck/gists{/gist_id}","starred_url":"https://api.github.com/users/natacha-beck/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/natacha-beck/subscriptions","organizations_url":"https://api.github.com/users/natacha-beck/orgs","repos_url":"https://api.github.com/users/natacha-beck/repos","events_url":"https://api.github.com/users/natacha-beck/events{/privacy}","received_events_url":"https://api.github.com/users/natacha-beck/received_events","type":"User","site_admin":false},"open_issues":68,"closed_issues":21,"state":"open","created_at":"2016-05-31T18:58:15Z","updated_at":"2016-07-07T20:51:04Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/aces/cbrain/pulls/420/commits","review_comments_url":"https://api.github.com/repos/aces/cbrain/pulls/420/comments","review_comment_url":"https://api.github.com/repos/aces/cbrain/pulls/comments{/number}","comments_url":"https://api.github.com/repos/aces/cbrain/issues/420/comments","statuses_url":"https://api.github.com/repos/aces/cbrain/statuses/2a306c5f4e134e487f2957b2ba02e20494b224b9","head":{"label":"ttaa9:boutiques_test","ref":"boutiques_test","sha":"2a306c5f4e134e487f2957b2ba02e20494b224b9","user":{"login":"ttaa9","id":6237441,"avatar_url":"https://avatars.githubusercontent.com/u/6237441?v=3","gravatar_id":"","url":"https://api.github.com/users/ttaa9","html_url":"https://github.com/ttaa9","followers_url":"https://api.github.com/users/ttaa9/followers","following_url":"https://api.github.com/users/ttaa9/following{/other_user}","gists_url":"https://api.github.com/users/ttaa9/gists{/gist_id}","starred_url":"https://api.github.com/users/ttaa9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ttaa9/subscriptions","organizations_url":"https://api.github.com/users/ttaa9/orgs","repos_url":"https://api.github.com/users/ttaa9/repos","events_url":"https://api.github.com/users/ttaa9/events{/privacy}","received_events_url":"https://api.github.com/users/ttaa9/received_events","type":"User","site_admin":false},"repo":{"id":58937342,"name":"cbrain","full_name":"ttaa9/cbrain","owner":{"login":"ttaa9","id":6237441,"avatar_url":"https://avatars.githubusercontent.com/u/6237441?v=3","gravatar_id":"","url":"https://api.github.com/users/ttaa9","html_url":"https://github.com/ttaa9","followers_url":"https://api.github.com/users/ttaa9/followers","following_url":"https://api.github.com/users/ttaa9/following{/other_user}","gists_url":"https://api.github.com/users/ttaa9/gists{/gist_id}","starred_url":"https://api.github.com/users/ttaa9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ttaa9/subscriptions","organizations_url":"https://api.github.com/users/ttaa9/orgs","repos_url":"https://api.github.com/users/ttaa9/repos","events_url":"https://api.github.com/users/ttaa9/events{/privacy}","received_events_url":"https://api.github.com/users/ttaa9/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ttaa9/cbrain","description":"CBRAIN is a flexible Ruby on Rails framework for accessing and processing of large data on high-performance computing infrastructures.","fork":true,"url":"https://api.github.com/repos/ttaa9/cbrain","forks_url":"https://api.github.com/repos/ttaa9/cbrain/forks","keys_url":"https://api.github.com/repos/ttaa9/cbrain/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ttaa9/cbrain/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ttaa9/cbrain/teams","hooks_url":"https://api.github.com/repos/ttaa9/cbrain/hooks","issue_events_url":"https://api.github.com/repos/ttaa9/cbrain/issues/events{/number}","events_url":"https://api.github.com/repos/ttaa9/cbrain/events","assignees_url":"https://api.github.com/repos/ttaa9/cbrain/assignees{/user}","branches_url":"https://api.github.com/repos/ttaa9/cbrain/branches{/branch}","tags_url":"https://api.github.com/repos/ttaa9/cbrain/tags","blobs_url":"https://api.github.com/repos/ttaa9/cbrain/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ttaa9/cbrain/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ttaa9/cbrain/git/refs{/sha}","trees_url":"https://api.github.com/repos/ttaa9/cbrain/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ttaa9/cbrain/statuses/{sha}","languages_url":"https://api.github.com/repos/ttaa9/cbrain/languages","stargazers_url":"https://api.github.com/repos/ttaa9/cbrain/stargazers","contributors_url":"https://api.github.com/repos/ttaa9/cbrain/contributors","subscribers_url":"https://api.github.com/repos/ttaa9/cbrain/subscribers","subscription_url":"https://api.github.com/repos/ttaa9/cbrain/subscription","commits_url":"https://api.github.com/repos/ttaa9/cbrain/commits{/sha}","git_commits_url":"https://api.github.com/repos/ttaa9/cbrain/git/commits{/sha}","comments_url":"https://api.github.com/repos/ttaa9/cbrain/comments{/number}","issue_comment_url":"https://api.github.com/repos/ttaa9/cbrain/issues/comments{/number}","contents_url":"https://api.github.com/repos/ttaa9/cbrain/contents/{+path}","compare_url":"https://api.github.com/repos/ttaa9/cbrain/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ttaa9/cbrain/merges","archive_url":"https://api.github.com/repos/ttaa9/cbrain/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ttaa9/cbrain/downloads","issues_url":"https://api.github.com/repos/ttaa9/cbrain/issues{/number}","pulls_url":"https://api.github.com/repos/ttaa9/cbrain/pulls{/number}","milestones_url":"https://api.github.com/repos/ttaa9/cbrain/milestones{/number}","notifications_url":"https://api.github.com/repos/ttaa9/cbrain/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ttaa9/cbrain/labels{/name}","releases_url":"https://api.github.com/repos/ttaa9/cbrain/releases{/id}","deployments_url":"https://api.github.com/repos/ttaa9/cbrain/deployments","created_at":"2016-05-16T14:06:30Z","updated_at":"2016-05-16T14:06:33Z","pushed_at":"2016-07-12T19:10:26Z","git_url":"git://github.com/ttaa9/cbrain.git","ssh_url":"git@github.com:ttaa9/cbrain.git","clone_url":"https://github.com/ttaa9/cbrain.git","svn_url":"https://github.com/ttaa9/cbrain","homepage":"","size":9066,"stargazers_count":0,"watchers_count":0,"language":"Ruby","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"aces:dev","ref":"dev","sha":"b11af2ded8c67d6f5a13cd518a9d57fe8a607ab3","user":{"login":"aces","id":472868,"avatar_url":"https://avatars.githubusercontent.com/u/472868?v=3","gravatar_id":"","url":"https://api.github.com/users/aces","html_url":"https://github.com/aces","followers_url":"https://api.github.com/users/aces/followers","following_url":"https://api.github.com/users/aces/following{/other_user}","gists_url":"https://api.github.com/users/aces/gists{/gist_id}","starred_url":"https://api.github.com/users/aces/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aces/subscriptions","organizations_url":"https://api.github.com/users/aces/orgs","repos_url":"https://api.github.com/users/aces/repos","events_url":"https://api.github.com/users/aces/events{/privacy}","received_events_url":"https://api.github.com/users/aces/received_events","type":"Organization","site_admin":false},"repo":{"id":32819225,"name":"cbrain","full_name":"aces/cbrain","owner":{"login":"aces","id":472868,"avatar_url":"https://avatars.githubusercontent.com/u/472868?v=3","gravatar_id":"","url":"https://api.github.com/users/aces","html_url":"https://github.com/aces","followers_url":"https://api.github.com/users/aces/followers","following_url":"https://api.github.com/users/aces/following{/other_user}","gists_url":"https://api.github.com/users/aces/gists{/gist_id}","starred_url":"https://api.github.com/users/aces/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aces/subscriptions","organizations_url":"https://api.github.com/users/aces/orgs","repos_url":"https://api.github.com/users/aces/repos","events_url":"https://api.github.com/users/aces/events{/privacy}","received_events_url":"https://api.github.com/users/aces/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/aces/cbrain","description":"CBRAIN is a flexible Ruby on Rails framework for accessing and processing of large data on high-performance computing infrastructures.","fork":false,"url":"https://api.github.com/repos/aces/cbrain","forks_url":"https://api.github.com/repos/aces/cbrain/forks","keys_url":"https://api.github.com/repos/aces/cbrain/keys{/key_id}","collaborators_url":"https://api.github.com/repos/aces/cbrain/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/aces/cbrain/teams","hooks_url":"https://api.github.com/repos/aces/cbrain/hooks","issue_events_url":"https://api.github.com/repos/aces/cbrain/issues/events{/number}","events_url":"https://api.github.com/repos/aces/cbrain/events","assignees_url":"https://api.github.com/repos/aces/cbrain/assignees{/user}","branches_url":"https://api.github.com/repos/aces/cbrain/branches{/branch}","tags_url":"https://api.github.com/repos/aces/cbrain/tags","blobs_url":"https://api.github.com/repos/aces/cbrain/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/aces/cbrain/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/aces/cbrain/git/refs{/sha}","trees_url":"https://api.github.com/repos/aces/cbrain/git/trees{/sha}","statuses_url":"https://api.github.com/repos/aces/cbrain/statuses/{sha}","languages_url":"https://api.github.com/repos/aces/cbrain/languages","stargazers_url":"https://api.github.com/repos/aces/cbrain/stargazers","contributors_url":"https://api.github.com/repos/aces/cbrain/contributors","subscribers_url":"https://api.github.com/repos/aces/cbrain/subscribers","subscription_url":"https://api.github.com/repos/aces/cbrain/subscription","commits_url":"https://api.github.com/repos/aces/cbrain/commits{/sha}","git_commits_url":"https://api.github.com/repos/aces/cbrain/git/commits{/sha}","comments_url":"https://api.github.com/repos/aces/cbrain/comments{/number}","issue_comment_url":"https://api.github.com/repos/aces/cbrain/issues/comments{/number}","contents_url":"https://api.github.com/repos/aces/cbrain/contents/{+path}","compare_url":"https://api.github.com/repos/aces/cbrain/compare/{base}...{head}","merges_url":"https://api.github.com/repos/aces/cbrain/merges","archive_url":"https://api.github.com/repos/aces/cbrain/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/aces/cbrain/downloads","issues_url":"https://api.github.com/repos/aces/cbrain/issues{/number}","pulls_url":"https://api.github.com/repos/aces/cbrain/pulls{/number}","milestones_url":"https://api.github.com/repos/aces/cbrain/milestones{/number}","notifications_url":"https://api.github.com/repos/aces/cbrain/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/aces/cbrain/labels{/name}","releases_url":"https://api.github.com/repos/aces/cbrain/releases{/id}","deployments_url":"https://api.github.com/repos/aces/cbrain/deployments","created_at":"2015-03-24T19:08:42Z","updated_at":"2016-07-08T05:26:01Z","pushed_at":"2016-07-12T19:10:27Z","git_url":"git://github.com/aces/cbrain.git","ssh_url":"git@github.com:aces/cbrain.git","clone_url":"https://github.com/aces/cbrain.git","svn_url":"https://github.com/aces/cbrain","homepage":"","size":9111,"stargazers_count":16,"watchers_count":16,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":12,"mirror_url":null,"open_issues_count":111,"forks":12,"open_issues":111,"watchers":16,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/aces/cbrain/pulls/420"},"html":{"href":"https://github.com/aces/cbrain/pull/420"},"issue":{"href":"https://api.github.com/repos/aces/cbrain/issues/420"},"comments":{"href":"https://api.github.com/repos/aces/cbrain/issues/420/comments"},"review_comments":{"href":"https://api.github.com/repos/aces/cbrain/pulls/420/comments"},"review_comment":{"href":"https://api.github.com/repos/aces/cbrain/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/aces/cbrain/pulls/420/commits"},"statuses":{"href":"https://api.github.com/repos/aces/cbrain/statuses/2a306c5f4e134e487f2957b2ba02e20494b224b9"}}}}
{"actor":{"display_login":"glatard"}}