From 13eda65390ea4cebcf8743a63e1f503a460ca8e7 Mon Sep 17 00:00:00 2001 From: Jarkad Date: Thu, 27 Nov 2025 19:42:19 +0000 Subject: [PATCH 1/7] Fix: FrozenError in project list To reproduce the issue, launch the web server with no configuration, add a project (its contents don't matter), and open http://127.0.0.1:8080/taskjuggler .
tj3webd output ``` $ tj3webd -d --debug TaskJuggler v3.8.4 - A Project Management Software Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 by Chris Schlaeger This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. [2025-11-27 20:30:10] INFO WEBrick 1.9.2 [2025-11-27 20:30:10] INFO ruby 3.4.1 (2024-12-25) [aarch64-linux-android] [2025-11-27 20:30:10] INFO WEBrick::HTTPServer#start: pid=7462 port=8080 127.0.0.1 - - [27/Nov/2025:20:30:36 CET] "GET /taskjuggler HTTP/1.1" 200 422 - -> /taskjuggler Exception 'WEBrick::HTTPStatus::EOFError' at /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/webrick-1.9.2/lib/webrick/httpserver.rb:82 - WEBrick::HTTPStatus::EOFError Exception 'FrozenError' at /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/daemon/ReportServlet.rb:191 - can't modify frozen String: "== Welcome to the TaskJuggler Project Server ==\n----\n" Error: Cannot serve GET request: GET /taskjuggler HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Mozilla/5.0 (Android 10; Mobile; rv:133.0) Gecko/133.0 Firefox/133.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US Accept-Encoding: gzip, deflate, br, zstd DNT: 1 Sec-GPC: 1 Connection: keep-alive Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: cross-site Priority: u=0, i can't modify frozen String: "== Welcome to the TaskJuggler Project Server ==\n----\n" 127.0.0.1 - - [27/Nov/2025:20:40:36 CET] "GET /taskjuggler HTTP/1.1" 412 563 - -> /taskjuggler [2025-11-27 20:40:36] ERROR SystemExit: exit /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/MessageHandler.rb:298:in 'Kernel#exit' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/MessageHandler.rb:298:in 'TaskJuggler::MessageHandlerInstance#addMessage' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/MessageHandler.rb:197:in 'TaskJuggler::MessageHandlerInstance#error' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/daemon/ReportServlet.rb:240:in 'TaskJuggler::ReportServlet#error' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/taskjuggler-3.8.4/lib/taskjuggler/daemon/ReportServlet.rb:62:in 'TaskJuggler::ReportServlet#do_GET' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/webrick-1.9.2/lib/webrick/httpservlet/abstract.rb:105:in 'WEBrick::HTTPServlet::AbstractServlet#service' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/webrick-1.9.2/lib/webrick/httpserver.rb:140:in 'WEBrick::HTTPServer#service' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/webrick-1.9.2/lib/webrick/httpserver.rb:96:in 'WEBrick::HTTPServer#run' /data/data/com.termux/files/usr/lib/ruby/gems/3.4.0/gems/webrick-1.9.2/lib/webrick/server.rb:309:in 'block in WEBrick::GenericServer#start_thread' ```
--- lib/taskjuggler/daemon/ReportServlet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taskjuggler/daemon/ReportServlet.rb b/lib/taskjuggler/daemon/ReportServlet.rb index d091bd6c..db224783 100644 --- a/lib/taskjuggler/daemon/ReportServlet.rb +++ b/lib/taskjuggler/daemon/ReportServlet.rb @@ -172,7 +172,7 @@ def generateWelcomePage(projectId) "Cannot get project list from daemon: #{$!}") end - text = "== Welcome to the TaskJuggler Project Server ==\n----\n" + text = +"== Welcome to the TaskJuggler Project Server ==\n----\n" projects.each do |id| if id == projectId # Show the list of reports for this project. From 3c2d55d59b456a5c38a4f29449ef5c3cb5cc67c7 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 18 May 2026 20:06:16 +0100 Subject: [PATCH 2/7] Fix: Daemon stdio redirection fails on Ruby 3.4+ In Ruby 3.4+, IO#reopen no longer accepts StringIO objects as an argument. It expects a String (file path) instead. This caused tj3d and tj3webd to fail with: Fatal: no implicit conversion of StringIO into String Change .reopen(StringIO.new) back to .reopen('/dev/null', 'a') which works on all Ruby versions. Reported-by: GitHub issue #303 Assisted-by: opencode/qwen3.6-plus Signed-off-by: Ankur Sinha (Ankur Sinha Gmail) --- lib/taskjuggler/daemon/Daemon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taskjuggler/daemon/Daemon.rb b/lib/taskjuggler/daemon/Daemon.rb index d8c06246..193bae62 100644 --- a/lib/taskjuggler/daemon/Daemon.rb +++ b/lib/taskjuggler/daemon/Daemon.rb @@ -69,7 +69,7 @@ def start # We no longer have a controlling terminal, so these are useless. $stdin.reopen('/dev/null') - $stdout.reopen(StringIO.new) + $stdout.reopen('/dev/null', 'a') $stderr.reopen($stdout) info('daemon_pid', From dc4f2fe53b41d28ad5867fb60bfd5d23b718fcd9 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 18 May 2026 20:14:55 +0100 Subject: [PATCH 3/7] Fix: ReportServlet StringIO buffers are read-only in web server context In Ruby 4.0+, StringIO.new('') unexpectedly creates a read-only buffer in the web server context. This caused report generation via the web interface to fail with: IOError: not opened for writing Use StringIO.new without arguments instead, which creates a writable buffer. Also add a missing return statement after the connect error to prevent further execution. Investigation: - StringIO.new('') works correctly in standalone Ruby scripts - StringIO.new('') works correctly in a forked daemon process - StringIO.new('') works correctly in a standalone WEBrick handler - The bug only manifests in the full TaskJuggler web server process, which involves multiple forks, DRb servers, and the complete TaskJuggler module stack loaded together - It is unclear why StringIO.new('') behaves differently in this context. Root cause not yet identified. Reported-by: GitHub issue #303 Assisted-by: opencode/qwen3.6-plus Signed-off-by: Ankur Sinha (Ankur Sinha Gmail) --- lib/taskjuggler/daemon/ReportServlet.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/taskjuggler/daemon/ReportServlet.rb b/lib/taskjuggler/daemon/ReportServlet.rb index db224783..ab5642d8 100644 --- a/lib/taskjuggler/daemon/ReportServlet.rb +++ b/lib/taskjuggler/daemon/ReportServlet.rb @@ -105,9 +105,13 @@ def generateReport(projectId, reportId, attributes) # text from the report server. This buffer will contain the generated # report as HTML encoded text. They will be send via DRb, so we have to # extend them with DRbUndumped. - stdOut = StringIO.new('') + # + # Note: In Ruby 4.0+, StringIO.new('') unexpectedly creates a read-only + # buffer in the web server context. Using StringIO.new without arguments + # avoids this issue. Root cause not yet identified. + stdOut = StringIO.new stdOut.extend(DRbUndumped) - stdErr = StringIO.new('') + stdErr = StringIO.new stdErr.extend(DRbUndumped) begin @@ -119,6 +123,7 @@ def generateReport(projectId, reportId, attributes) end error('rs_io_connect_failed', "Can't connect IO: #{$!}") + return end # Ask the ReportServer to generate the reports with the provided ID. From 50f2ad69ddc3d79c30acf99e68362a6f925283c6 Mon Sep 17 00:00:00 2001 From: fehard Date: Wed, 23 Oct 2024 11:34:38 +0200 Subject: [PATCH 4/7] Fixed: Multithreading exception --- lib/taskjuggler/BatchProcessor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taskjuggler/BatchProcessor.rb b/lib/taskjuggler/BatchProcessor.rb index 81da1437..fa9b95fa 100644 --- a/lib/taskjuggler/BatchProcessor.rb +++ b/lib/taskjuggler/BatchProcessor.rb @@ -249,7 +249,7 @@ def receiver # Remove the job from the @runningJobs Hash. @runningJobs.delete(pid) # Save the return value. - job.retVal = retVal.exitstatus + job.retVal = retVal if retVal.signaled? cleanPipes(job) # Aborted jobs will probably not send an EOT. So we fastrack From 2bfa5f7e6b433e310db387fa13692e40d12cf1ad Mon Sep 17 00:00:00 2001 From: Enno Richter <2536303+elohmeier@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:21:29 +0200 Subject: [PATCH 5/7] build: support Ruby 4 --- .github/workflows/ruby.yml | 43 ++++++ .gitignore | 1 + Gemfile | 3 + spec/ReportServlet_spec.rb | 86 ++++++++++++ spec/support/DaemonControl.rb | 2 +- taskjuggler.gemspec | 25 ++-- test/TestSuite/Export-Reports/refs/Leave.tjp | 69 ++++++++++ .../Export-Reports/refs/ListAttributes.tjp | 66 +++++++++ .../TestSuite/Export-Reports/refs/Macro-4.tjp | 18 +++ .../Export-Reports/refs/Timezone2.tjp | 18 +++ .../Export-Reports/refs/TraceReport.tjp | 34 +++++ test/test_BatchProcessor.rb | 4 +- test/web_server_smoke.rb | 128 ++++++++++++++++++ 13 files changed, 481 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/ruby.yml create mode 100644 Gemfile create mode 100644 spec/ReportServlet_spec.rb create mode 100644 test/TestSuite/Export-Reports/refs/Leave.tjp create mode 100644 test/TestSuite/Export-Reports/refs/ListAttributes.tjp create mode 100644 test/TestSuite/Export-Reports/refs/Macro-4.tjp create mode 100644 test/TestSuite/Export-Reports/refs/Timezone2.tjp create mode 100644 test/TestSuite/Export-Reports/refs/TraceReport.tjp create mode 100644 test/web_server_smoke.rb diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 00000000..e75bebd1 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,43 @@ +name: Ruby + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.2' + - '3.3' + - '3.4' + - '4.0' + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run unit and specification tests + run: bundle exec rake test + - name: Check and schedule a project + run: | + bundle exec ruby -Ilib lib/tj3.rb --silent --check-syntax test/TestSuite/Syntax/Correct/Simple.tjp + bundle exec ruby -Ilib lib/tj3.rb --silent --no-reports test/TestSuite/Syntax/Correct/Simple.tjp + bundle exec ruby -Ilib lib/tj3.rb --silent --no-reports --list-reports '.*' test/TestSuite/Syntax/Correct/tutorial.tjp + - name: Generate reports with multiple workers + run: | + mkdir -p "${RUNNER_TEMP}/taskjuggler-reports" + bundle exec ruby -Ilib lib/tj3.rb --silent -c 2 \ + --output-dir "${RUNNER_TEMP}/taskjuggler-reports" \ + test/TestSuite/Syntax/Correct/tutorial.tjp + test "$(find "${RUNNER_TEMP}/taskjuggler-reports" -type f | wc -l)" -eq 20 + - name: Exercise the daemon and web server + run: bundle exec ruby test/web_server_smoke.rb diff --git a/.gitignore b/.gitignore index 4e61ab87..befb585f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ benchmarks/css benchmarks/icons benchmarks/scripts +/Gemfile.lock CHANGELOG doc lib/css diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..fa75df15 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/spec/ReportServlet_spec.rb b/spec/ReportServlet_spec.rb new file mode 100644 index 00000000..2a31a251 --- /dev/null +++ b/spec/ReportServlet_spec.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby -w +# frozen_string_literal: true +# encoding: UTF-8 +# +# = ReportServlet_spec.rb -- The TaskJuggler III Project Management Software +# +# Copyright (c) 2026 Enno Richter <2536303+elohmeier@users.noreply.github.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# + +require 'taskjuggler/Tj3Config' +require 'taskjuggler/daemon/ReportServlet' + +class TaskJuggler + + describe ReportServlet do + + def response + Class.new do + attr_accessor :body, :status + + def initialize + @headers = {} + end + + def [](key) + @headers[key] + end + + def []=(key, value) + @headers[key] = value + end + end.new + end + + it 'generates a welcome page with projects when string literals are frozen' do + broker = double('broker', :getProjectList => [ 'example' ], + :disconnect => nil) + servlet = ReportServlet.allocate + res = response + servlet.instance_variable_set(:@res, res) + allow(AppConfig).to receive(:appName).and_return('tj3webd') + allow(servlet).to receive(:connectToBroker).and_return(broker) + allow(servlet).to receive(:getProjectName).with('example'). + and_return('Example Project') + + servlet.send(:generateWelcomePage, '') + + res['content-type'].should eq('text/html') + res.body.should match(/Example Project/) + end + + it 'uses writable buffers while generating a report' do + broker = double('broker', :getProject => [ 'project-uri', 'project-key' ], + :disconnect => nil) + projectServer = double('project server', + :getReportServer => [ 'report-uri', 'report-key' ]) + reportServer = double('report server') + allow(reportServer).to receive(:connect) do |_key, stdOut, stdErr, + _stdIn, _silent| + stdOut.write('Generated report') + stdErr.write('') + end + allow(reportServer).to receive(:generateReport).and_return(true) + allow(reportServer).to receive(:disconnect) + allow(reportServer).to receive(:terminate) + allow(DRbObject).to receive(:new). + and_return(projectServer, reportServer) + + servlet = ReportServlet.allocate + res = response + servlet.instance_variable_set(:@res, res) + allow(servlet).to receive(:connectToBroker).and_return(broker) + + servlet.send(:generateReport, 'example', 'report', '') + + res['content-type'].should eq('text/html') + res.body.should eq('Generated report') + end + + end + +end diff --git a/spec/support/DaemonControl.rb b/spec/support/DaemonControl.rb index f88d9d9d..dbd9394b 100644 --- a/spec/support/DaemonControl.rb +++ b/spec/support/DaemonControl.rb @@ -41,7 +41,7 @@ def startDaemon(config = '') $stdout.reopen('stdout.log', 'w') $stderr.reopen('stderr.log', 'w') res = stdIoWrapper do - Tj3Daemon.new.main(%w( --silent )) + Tj3Daemon.new.main(%w( --silent --dont-daemonize )) end raise "Failed to start tj3d: #{res.stdErr}" if res.returnValue != 0 exit! diff --git a/taskjuggler.gemspec b/taskjuggler.gemspec index 3232b86f..61f87622 100644 --- a/taskjuggler.gemspec +++ b/taskjuggler.gemspec @@ -15,15 +15,10 @@ lib = File.expand_path('../lib', __FILE__) $:.unshift lib unless $:.include?(lib) -# Get software version number from Tj3Config class. -begin - $: << 'lib' - require 'taskjuggler/Tj3Config' - PROJECT_VERSION = AppConfig.version - PROJECT_NAME = AppConfig.softwareName -rescue LoadError - raise "Error: Cannot determine software settings: #{$!}" -end +# Keep gem metadata loadable before runtime dependencies have been installed. +require_relative 'lib/taskjuggler/version' +PROJECT_VERSION = VERSION +PROJECT_NAME = 'TaskJuggler' GEM_SPEC = Gem::Specification.new { |s| s.name = 'taskjuggler' @@ -52,19 +47,23 @@ EOT (`git ls-files -- tasks`).split("\n") + %w( .gemtest taskjuggler.gemspec Rakefile ) + # Generated files, not contained in Git repository. - %w( data/tjp.vim ) + Dir.glob('manual/html/**/*') + Dir.glob('man/*.1') + Dir.glob('manual/html/**/*') + Dir.glob('man/*.1') s.bindir = 'bin' s.executables = (`git ls-files -- bin`).split("\n"). map { |fn| File.basename(fn) } s.test_files = (`git ls-files -- test`).split("\n") + (`git ls-files -- spec`).split("\n") - s.extra_rdoc_files = %w( README.rdoc COPYING CHANGELOG ) + s.extra_rdoc_files = %w( README.rdoc COPYING ) + s.add_dependency('base64', '>= 0.2.0') + s.add_dependency('drb', '>= 2.1.0') s.add_dependency('mail', '~> 2.7', '>= 2.7.1') s.add_dependency('webrick', '~> 1.9', '>= 1.9.1') s.add_runtime_dependency('term-ansicolor', '~> 1.7', '>= 1.7.1') - s.add_development_dependency('rspec', '~> 2.5', '>= 2.5.0') + s.add_development_dependency('rake', '~> 13.0') + s.add_development_dependency('rspec', '~> 3.13') + s.add_development_dependency('test-unit', '~> 3.7') s.platform = Gem::Platform::RUBY - s.required_ruby_version = '>= 2.0.0' + s.required_ruby_version = '>= 3.2.0' } diff --git a/test/TestSuite/Export-Reports/refs/Leave.tjp b/test/TestSuite/Export-Reports/refs/Leave.tjp new file mode 100644 index 00000000..d75ddbfa --- /dev/null +++ b/test/TestSuite/Export-Reports/refs/Leave.tjp @@ -0,0 +1,69 @@ +project prj "Annual Leave" "1.0" 2011-12-19-00:00-+0000 - 2012-12-18-00:00-+0000 { + timezone "UTC" + scenario plan "Plan Scenario" { + active yes + } +} + +projectids prj + +shift s1 "Shift 1" { + workinghours sun off + workinghours mon 9:00 - 17:00 + workinghours tue 9:00 - 17:00 + workinghours wed 9:00 - 17:00 + workinghours thu 9:00 - 17:00 + workinghours fri 9:00 - 17:00 + workinghours sat off +} +resource team "Team" { + resource r1 "R1" + resource r2 "R2" +} +resource r3 "R3" + +task _Task_1 "foo" { + start 2011-12-19-00:00-+0000 + scheduled +} +supplement task _Task_1 { + priority 500 + projectid prj +} +supplement resource team { + workinghours sun off + workinghours mon 9:00 - 17:00 + workinghours tue 9:00 - 17:00 + workinghours wed 9:00 - 17:00 + workinghours thu 9:00 - 17:00 + workinghours fri 9:00 - 17:00 + workinghours sat off +} +supplement resource r1 { + workinghours sun off + workinghours mon 9:00 - 17:00 + workinghours tue 9:00 - 17:00 + workinghours wed 9:00 - 17:00 + workinghours thu 9:00 - 17:00 + workinghours fri 9:00 - 17:00 + workinghours sat off +} +supplement resource r2 { + workinghours sun off + workinghours mon 9:00 - 17:00 + workinghours tue 9:00 - 17:00 + workinghours wed 9:00 - 17:00 + workinghours thu 9:00 - 17:00 + workinghours fri 9:00 - 17:00 + workinghours sat off +} +supplement resource r3 { + shifts s1 2011-12-19-00:00-+0000 - 2012-01-09-00:00-+0000 + workinghours sun off + workinghours mon 9:00 - 17:00 + workinghours tue 9:00 - 17:00 + workinghours wed 9:00 - 17:00 + workinghours thu 9:00 - 17:00 + workinghours fri 9:00 - 17:00 + workinghours sat off +} diff --git a/test/TestSuite/Export-Reports/refs/ListAttributes.tjp b/test/TestSuite/Export-Reports/refs/ListAttributes.tjp new file mode 100644 index 00000000..d8664eaa --- /dev/null +++ b/test/TestSuite/Export-Reports/refs/ListAttributes.tjp @@ -0,0 +1,66 @@ +project prj "List attributes" "1.0" 2014-04-06-00:00-+0000 - 2014-05-06-10:00-+0000 { + timezone "UTC" + scenario s1 "S1" { + scenario s2 "S2" { + active yes + } + active yes + } +} + +flags f1, f2, f3 + +projectids prj + + +task _Task_1 "T1" { + start 2014-04-06-00:00-+0000 + scheduled +} +task _Task_2 "T2" { + task _Task_3 "T3" { + start 2014-04-06-00:00-+0000 + scheduled + } +} +task _Task_4 "T4" { + start 2014-04-06-00:00-+0000 + scheduled +} +task _Task_5 "T5" { + task _Task_6 "T6" { + start 2014-04-06-00:00-+0000 + scheduled + } +} +supplement task _Task_1 { + flags f1, f2, f3 + priority 500 + projectid prj +} +supplement task _Task_2 { + flags f1 + priority 500 + projectid prj +} +supplement task _Task_2._Task_3 { + flags f1, f2 + priority 500 + projectid prj +} +supplement task _Task_4 { + flags f1 + s2:flags f1, f2 + priority 500 + projectid prj +} +supplement task _Task_5 { + flags f1 + priority 500 + projectid prj +} +supplement task _Task_5._Task_6 { + flags f2 + priority 500 + projectid prj +} diff --git a/test/TestSuite/Export-Reports/refs/Macro-4.tjp b/test/TestSuite/Export-Reports/refs/Macro-4.tjp new file mode 100644 index 00000000..5a1a7af0 --- /dev/null +++ b/test/TestSuite/Export-Reports/refs/Macro-4.tjp @@ -0,0 +1,18 @@ +project prj "Test" "1.0" 2012-08-17-00:00-+0000 - 2012-09-16-10:00-+0000 { + timezone "UTC" + scenario plan "Plan Scenario" { + active yes + } +} + +projectids prj + + +task t "T" { + start 2012-08-17-00:00-+0000 + scheduled +} +supplement task t { + priority 500 + projectid prj +} diff --git a/test/TestSuite/Export-Reports/refs/Timezone2.tjp b/test/TestSuite/Export-Reports/refs/Timezone2.tjp new file mode 100644 index 00000000..20c6ad4d --- /dev/null +++ b/test/TestSuite/Export-Reports/refs/Timezone2.tjp @@ -0,0 +1,18 @@ +project tz "Timezone" "1.0" 2005-06-05-11:00-+0000 - 2005-06-06-11:00-+0000 { + timezone "Pacific/Auckland" + scenario plan "Plan Scenario" { + active yes + } +} + +projectids tz + + +task item "Project" { + start 2005-06-06-00:00-+0000 + scheduled +} +supplement task item { + priority 500 + projectid tz +} diff --git a/test/TestSuite/Export-Reports/refs/TraceReport.tjp b/test/TestSuite/Export-Reports/refs/TraceReport.tjp new file mode 100644 index 00000000..3acb8dd5 --- /dev/null +++ b/test/TestSuite/Export-Reports/refs/TraceReport.tjp @@ -0,0 +1,34 @@ +project prj "Trace Reports" "1.0" 2012-01-14-00:00-+0000 - 2012-03-14-20:00-+0000 { + timezone "UTC" + scenario plan "Plan Scenario" { + active yes + } +} + +projectids prj + + +task _Task_1 "Foo" { + start 2012-01-14-00:00-+0000 + scheduled +} +task _Task_2 "Bar" { + start 2012-01-14-00:00-+0000 + scheduled +} +task _Task_3 "FooBar" { + start 2012-01-14-00:00-+0000 + scheduled +} +supplement task _Task_1 { + priority 500 + projectid prj +} +supplement task _Task_2 { + priority 500 + projectid prj +} +supplement task _Task_3 { + priority 500 + projectid prj +} diff --git a/test/test_BatchProcessor.rb b/test/test_BatchProcessor.rb index 3c2184c4..c5e1c1dc 100644 --- a/test/test_BatchProcessor.rb +++ b/test/test_BatchProcessor.rb @@ -84,7 +84,8 @@ def runJob(n, &block) end def postprocess(job) - assert_equal(job.retVal, job.jobId, 'PID mismatch') + assert_kind_of(Process::Status, job.retVal) + assert_equal(job.jobId, job.retVal.exitstatus, 'PID mismatch') assert_equal("job #{job.jobId}", job.tag) text = <<"EOT" Job #{job.jobId} started @@ -99,4 +100,3 @@ def postprocess(job) assert_equal(text, job.stderr, "STDERR mismatch #{job.stderr}") end end - diff --git a/test/web_server_smoke.rb b/test/web_server_smoke.rb new file mode 100644 index 00000000..7bdb3c55 --- /dev/null +++ b/test/web_server_smoke.rb @@ -0,0 +1,128 @@ +#!/usr/bin/env ruby -w +# frozen_string_literal: true +# encoding: UTF-8 +# +# = web_server_smoke.rb -- The TaskJuggler III Project Management Software +# +# Copyright (c) 2026 Enno Richter <2536303+elohmeier@users.noreply.github.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# + +require 'net/http' +require 'rbconfig' +require 'fileutils' +require 'socket' +require 'timeout' +require 'tmpdir' + +def freePort + server = TCPServer.new('127.0.0.1', 0) + port = server.addr[1] + server.close + port +end + +def waitForPort(port) + Timeout.timeout(15) do + loop do + socket = TCPSocket.new('127.0.0.1', port) + socket.close + return + rescue Errno::ECONNREFUSED + sleep 0.1 + end + end +end + +def stopProcess(pid) + return unless pid + + begin + Process.kill('TERM', pid) + rescue Errno::ESRCH + end + + begin + Timeout.timeout(5) { Process.wait(pid) } + rescue Errno::ECHILD + rescue Timeout::Error + begin + Process.kill('KILL', pid) + Process.wait(pid) + rescue Errno::ESRCH, Errno::ECHILD + end + end +end + +sourceDir = File.expand_path('..', __dir__) +sourceProjectFile = File.join(sourceDir, + 'test/TestSuite/Syntax/Correct/textreport.tjp') +daemonPort = freePort +webPort = freePort + +Dir.mktmpdir('taskjuggler-web-smoke-') do |runDir| + configFile = File.join(runDir, 'taskjuggler.rc') + daemonLog = File.join(runDir, 'daemon.log') + projectFile = File.join(runDir, 'textreport.tjp') + webLog = File.join(runDir, 'web.log') + File.write(configFile, "_global:\n authKey: ruby-compatibility-smoke\n") + FileUtils.cp(sourceProjectFile, projectFile) + + daemonPid = nil + webPid = nil + begin + daemonPid = Process.spawn(RbConfig.ruby, "-I#{sourceDir}/lib", + File.join(sourceDir, 'lib/tj3d.rb'), + '--silent', '--no-color', '--dont-daemonize', + '--config', configFile, + '--port', daemonPort.to_s, + projectFile, + :chdir => runDir, + :out => daemonLog, :err => [ :child, :out ]) + waitForPort(daemonPort) + + webPid = Process.spawn(RbConfig.ruby, "-I#{sourceDir}/lib", + File.join(sourceDir, 'lib/tj3webd.rb'), + '--silent', '--no-color', '--dont-daemonize', + '--config', configFile, + '--port', daemonPort.to_s, + '--webserver-port', webPort.to_s, + :chdir => runDir, + :out => webLog, :err => [ :child, :out ]) + waitForPort(webPort) + + base = URI("http://127.0.0.1:#{webPort}") + welcome = Net::HTTP.get_response(base + '/taskjuggler') + unless welcome.code == '200' + raise "Welcome page returned HTTP #{welcome.code}: #{welcome.body}" + end + + projectPath = welcome.body[/href="([^"]*project=[^"]*)"/, 1] + raise 'Welcome page has no project link' unless projectPath + + projectPage = Net::HTTP.get_response(base + projectPath.gsub('&', '&')) + unless projectPage.code == '200' + raise "Project page returned HTTP #{projectPage.code}: #{projectPage.body}" + end + + reportPath = projectPage.body[/href="([^"]*report=[^"]*)"/, 1] + raise 'Project page has no report link' unless reportPath + + reportPage = Net::HTTP.get_response(base + reportPath.gsub('&', '&')) + unless reportPage.code == '200' && reportPage.body.include?(' Date: Sat, 1 Aug 2026 11:24:48 +0200 Subject: [PATCH 6/7] ci: use Node 24 checkout action --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index e75bebd1..0640f684 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -20,7 +20,7 @@ jobs: - '4.0' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} From 16f2bc46366ce93a94020c82c77bd96e9a56f8a4 Mon Sep 17 00:00:00 2001 From: Enno Richter <2536303+elohmeier@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:39:43 +0200 Subject: [PATCH 7/7] build: support gitless source archives --- taskjuggler.gemspec | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/taskjuggler.gemspec b/taskjuggler.gemspec index 61f87622..e24d4804 100644 --- a/taskjuggler.gemspec +++ b/taskjuggler.gemspec @@ -20,6 +20,11 @@ require_relative 'lib/taskjuggler/version' PROJECT_VERSION = VERSION PROJECT_NAME = 'TaskJuggler' +filesIn = lambda do |directory| + files = (`git ls-files -- #{directory} 2>/dev/null`).split("\n") + files.empty? ? Dir.glob("#{directory}/**/*").select { |f| File.file?(f) } : files +end + GEM_SPEC = Gem::Specification.new { |s| s.name = 'taskjuggler' s.version = PROJECT_VERSION @@ -40,19 +45,18 @@ management. EOT s.license = 'GPL-2.0-only' s.require_path = 'lib' - s.files = (`git ls-files -- lib`).split("\n") + - (`git ls-files -- data`).split("\n") + - (`git ls-files -- manual`).split("\n") + - (`git ls-files -- examples`).split("\n") + - (`git ls-files -- tasks`).split("\n") + + s.files = filesIn.call('lib') + + filesIn.call('data') + + filesIn.call('manual') + + filesIn.call('examples') + + filesIn.call('tasks') + %w( .gemtest taskjuggler.gemspec Rakefile ) + # Generated files, not contained in Git repository. Dir.glob('manual/html/**/*') + Dir.glob('man/*.1') s.bindir = 'bin' - s.executables = (`git ls-files -- bin`).split("\n"). + s.executables = filesIn.call('bin'). map { |fn| File.basename(fn) } - s.test_files = (`git ls-files -- test`).split("\n") + - (`git ls-files -- spec`).split("\n") + s.test_files = filesIn.call('test') + filesIn.call('spec') s.extra_rdoc_files = %w( README.rdoc COPYING )