track_open_instances 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 264fbcefa796af11230c90bc8f1221363d3af1515975e8647a694a486499c874
4
+ data.tar.gz: 7a5129b04a6ff432a8b7641f80b0b18e98ea52a96b8bb45599ba991650558fea
5
+ SHA512:
6
+ metadata.gz: '0963b6a2fcd9fb91f42dfadc663425365de598b82e1635d8178216ca05da85d19cf1ba8c0bf67807bf3ff650b73558717703ade5772bb516d357b2c2517e363c'
7
+ data.tar.gz: 57aa5b10f7fc3b96f062cab47985ac5e060aebff83a408293c91339809f68105414b34bff748a016a53f055b23bbf30f37ae3dde43ce05c24a660d59896382f4
data/.husky/commit-msg ADDED
@@ -0,0 +1 @@
1
+ npx --no-install commitlint --edit "$1"
data/.markdownlint.yml ADDED
@@ -0,0 +1,26 @@
1
+ ---
2
+ default: true
3
+
4
+ # Unordered list indentation
5
+ MD007: { indent: 2 }
6
+
7
+ # Line length
8
+ MD013: { line_length: 90, tables: false, code_blocks: false }
9
+
10
+ # Heading duplication is allowed for non-sibling headings
11
+ MD024: { siblings_only: true }
12
+
13
+ # Do not allow the specified trailing punctuation in a header
14
+ MD026: { punctuation: ".,;:" }
15
+
16
+ # Order list items must have a prefix that increases in numerical order
17
+ MD029: { style: "ordered" }
18
+
19
+ # Lists do not need to be surrounded by blank lines
20
+ MD032: false
21
+
22
+ # Allow raw HTML in Markdown
23
+ MD033: false
24
+
25
+ # Allow emphasis to be used instead of a heading
26
+ MD036: false
data/.qlty/qlty.toml ADDED
@@ -0,0 +1,104 @@
1
+ # This file was automatically generated by `qlty init`.
2
+ # You can modify it to suit your needs.
3
+ # We recommend you to commit this file to your repository.
4
+ #
5
+ # This configuration is used by both Qlty CLI and Qlty Cloud.
6
+ #
7
+ # Qlty CLI -- Code quality toolkit for developers
8
+ # Qlty Cloud -- Fully automated Code Health Platform
9
+ #
10
+ # Try Qlty Cloud: https://qlty.sh
11
+ #
12
+ # For a guide to configuration, visit https://qlty.sh/d/config
13
+ # Or for a full reference, visit https://qlty.sh/d/qlty-toml
14
+ config_version = "0"
15
+
16
+ exclude_patterns = [
17
+ "*_min.*",
18
+ "*-min.*",
19
+ "*.min.*",
20
+ "**/*.d.ts",
21
+ "**/.yarn/**",
22
+ "**/bower_components/**",
23
+ "**/build/**",
24
+ "**/cache/**",
25
+ "**/config/**",
26
+ "**/db/**",
27
+ "**/deps/**",
28
+ "**/dist/**",
29
+ "**/extern/**",
30
+ "**/external/**",
31
+ "**/generated/**",
32
+ "**/Godeps/**",
33
+ "**/gradlew/**",
34
+ "**/mvnw/**",
35
+ "**/node_modules/**",
36
+ "**/protos/**",
37
+ "**/seed/**",
38
+ "**/target/**",
39
+ "**/testdata/**",
40
+ "**/vendor/**",
41
+ "**/assets/**",
42
+ ]
43
+
44
+ test_patterns = [
45
+ "**/test/**",
46
+ "**/spec/**",
47
+ "**/*.test.*",
48
+ "**/*.spec.*",
49
+ "**/*_test.*",
50
+ "**/*_spec.*",
51
+ "**/test_*.*",
52
+ "**/spec_*.*",
53
+ ]
54
+
55
+ [smells]
56
+ mode = "comment"
57
+
58
+ [[source]]
59
+ name = "default"
60
+ default = true
61
+
62
+ [[plugin]]
63
+ name = "actionlint"
64
+
65
+ [[plugin]]
66
+ name = "checkov"
67
+
68
+ [[plugin]]
69
+ name = "golangci-lint"
70
+ mode = "comment"
71
+
72
+ [[plugin]]
73
+ name = "markdownlint"
74
+ version = "0.41.0"
75
+ mode = "comment"
76
+
77
+ [[plugin]]
78
+ name = "prettier"
79
+
80
+ # [[plugin]]
81
+ # name = "radarlint-ruby"
82
+
83
+ [[plugin]]
84
+ name = "ripgrep"
85
+ mode = "comment"
86
+
87
+ # [[plugin]]
88
+ # name = "rubocop"
89
+ # version = "1.75.2"
90
+ # package_file = "Gemfile"
91
+ # package_filters = ["rubocop"]
92
+
93
+ [[plugin]]
94
+ name = "trivy"
95
+ drivers = [
96
+ "config",
97
+ "fs-vuln",
98
+ ]
99
+
100
+ [[plugin]]
101
+ name = "trufflehog"
102
+
103
+ [[plugin]]
104
+ name = "yamllint"
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ inherit_gem:
3
+ main_branch_shared_rubocop_config: config/rubocop.yml
4
+
5
+ Metrics/MethodLength:
6
+ Exclude:
7
+ - "spec/spec_helper.rb"
8
+ - "spec/**/*_spec.rb"
9
+
10
+ Metrics/AbcSize:
11
+ Exclude:
12
+ - "spec/spec_helper.rb"
13
+ - "spec/**/*_spec.rb"
14
+
15
+ AllCops:
16
+ # Pin this project to Ruby 3.1 in case the shared config above is
17
+ # upgraded to 3.2 or later.
18
+ TargetRubyVersion: 3.1
data/.yamllint.yml ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ extends: default
3
+
4
+ ignore: |
5
+ node_modules
6
+
7
+ rules:
8
+ braces:
9
+ min-spaces-inside: 1
10
+ max-spaces-inside: 1
11
+ truthy:
12
+ check-keys: false
data/.yardopts ADDED
@@ -0,0 +1,9 @@
1
+ --no-private
2
+ --hide-void-return
3
+ --markup-provider=redcarpet
4
+ --markup markdown
5
+ --readme README.md
6
+ - CHANGELOG.md
7
+ - CONTRIBUTING.md
8
+ - RELEASING.md
9
+ - LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Change Log
2
+
3
+ ## v0.1.0 (2025-04-08)
4
+
5
+ [Full Changelog](https://github.com/main-branch/track_open_instances/compare/f0fb459..v0.1.0)
6
+
7
+ Changes:
8
+
9
+ - f2d5318 chore: remove radarline-ruby from qlty checks
10
+ - 177eba8 chore: fix formatting errors reported by `qlty fmt`
11
+ - 943359f build: remove rubocop plugin from qlty
12
+ - 476f271 chore: integrate yamllint
13
+ - 56811ca fix: add qlty configuration in order to run qlty checks locally
14
+ - e83546a fix: fix continuous integration workflow issues reported by qlty.sh
15
+ - 6f67aed test: add predicate methods to determine the Ruby engine and platform
16
+ - 34e3829 build: generate JSON coverage report for qlty.sh coverage reporting
17
+ - 9db2052 docs: update README links
18
+ - 937a971 build: update code coverage reporting
19
+ - f0fb459 feat: initial implementation of TrackOpenInstances
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,107 @@
1
+ # Contributing to track_open_instances
2
+
3
+ Thank you for your interest in contributing to the track_open_instances project!
4
+
5
+ This document gives the guidelines for contributing to this project.
6
+ These guidelines may not fit every situation. When contributing use your best
7
+ judgement.
8
+
9
+ Propose changes to these guidelines with a pull request.
10
+
11
+ ## How to contribute to track_open_instances
12
+
13
+ You can contribute in two ways:
14
+
15
+ 1. [Report an issue or make a feature request](#how-to-report-an-issue-or-make-a-feature-request)
16
+ 2. [Submit a code or documentation change](#how-to-submit-a-code-or-documentation-change)
17
+
18
+ ## How to report an issue or make a feature request
19
+
20
+ track_open_instances utilizes [GitHub Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues)
21
+ for issue tracking and feature requests.
22
+
23
+ Report an issue or feature request by [creating a track_open_instances Github issue](https://github.com/main-branch/track_open_instances/issues/new).
24
+ Fill in the template to describe the issue or feature request the best you can.
25
+
26
+ ## How to submit a code or documentation change
27
+
28
+ There is three step process for code or documentation changes:
29
+
30
+ 1. [Commit your changes to a fork of track_open_instances](#commit-changes-to-a-fork-of-track_open_instances)
31
+ 2. [Create a pull request](#create-a-pull-request)
32
+ 3. [Get your pull request reviewed](#get-your-pull-request-reviewed)
33
+
34
+ ### Commit changes to a fork of track_open_instances
35
+
36
+ Make your changes in a fork of the track_open_instances repository.
37
+
38
+ ### Create a pull request
39
+
40
+ See [this article](https://help.github.com/articles/about-pull-requests/) if you
41
+ are not familiar with GitHub Pull Requests.
42
+
43
+ Follow the instructions in the pull request template.
44
+
45
+ ### Get your pull request reviewed
46
+
47
+ Code review takes place in a GitHub pull request using the
48
+ [the Github pull request review feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews).
49
+
50
+ Once your pull request is ready for review, request a review from at least one of the
51
+ [code owners](https://github.com/orgs/main-branch/teams/track_open_instances-codeowners/members).
52
+
53
+ During the review process, you may need to make additional commits which would
54
+ need to be squashed. It may also be necessary to rebase to main again if other
55
+ changes are merged before your PR.
56
+
57
+ At least one approval is required from a project maintainer before your pull
58
+ request can be merged. The maintainer is responsible for ensuring that the pull
59
+ request meets [the project's coding standards](#coding-standards).
60
+
61
+ ## Coding standards
62
+
63
+ All pull requests must meet these requirements:
64
+
65
+ ### 1 PR = 1 Commit
66
+
67
+ - All commits for a PR must be squashed into one commit
68
+ - To avoid an extra merge commit, the PR must be able to be merged as [a fast forward merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging)
69
+ - The easiest way to ensure a fast forward merge is to rebase your local branch
70
+ to the track_open_instances main branch
71
+
72
+ ### Unit tests
73
+
74
+ - All changes must be accompanied by new or modified RSpec unit tests
75
+ - The entire test suite must pass when `bundle exec rake spec` is run from the
76
+ project's local working tree
77
+ - The unit test suite must maintain 100% code coverage to pass
78
+
79
+ ### Documentation
80
+
81
+ - New and updated public methods must have [YARD](https://yardoc.org/)
82
+ documentation added to them
83
+ - [The YARD Cheatsheet](https://gist.github.com/thelastinuit/5984665e6ab69d3c0a413a03602c45be)
84
+ is a good reference
85
+ - New and updated public facing features should be documented in the project's
86
+ [README.md](README.md)
87
+ - All documentation must pass `yardstick` documentation analysis
88
+ - The documentation suite must maintain 100% documentation to pass
89
+
90
+ ### Continuous Integration
91
+
92
+ - All tests must pass in the project's [Travis CI](https://travis-ci.org/main-branch/track_open_instances)
93
+ build before the pull request will be merged.
94
+ - You can simulate what happens in the Travis CI build by running `bundle exec rake` in
95
+ the projects root directory.
96
+
97
+ ### Other Design Guidelines
98
+
99
+ - Use keyword args with defaults instead of an opts hash
100
+
101
+ ## Licensing
102
+
103
+ track_open_instances uses [the MIT license](https://choosealicense.com/licenses/mit/) as
104
+ declared in the [LICENSE](LICENSE) file.
105
+
106
+ Licensing is very important to open source projects. It helps ensure the
107
+ software continues to be available under the terms that the author desired.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 James Couball
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # track_open_instances gem
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/track_open_instances.svg)](https://badge.fury.io/rb/track_open_instances)
4
+ [![Build Status](https://github.com/main-branch/track_open_instances/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/main-branch/track_open_instances/actions/workflows/continuous_integration.yml)
5
+ [![Documentation](https://img.shields.io/badge/Documentation-Latest-green)](https://rubydoc.info/gems/track_open_instances/)
6
+ [![Change Log](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://rubydoc.info/gems/track_open_instances/file/CHANGELOG.md)
7
+ [![Maintainability](https://qlty.sh/badges/52f7402e-ab92-4f8f-9f62-d879ca628adb/maintainability.svg)](https://qlty.sh/gh/main-branch/projects/track_open_instances)
8
+ [![Code Coverage](https://qlty.sh/badges/52f7402e-ab92-4f8f-9f62-d879ca628adb/test_coverage.svg)](https://qlty.sh/gh/main-branch/projects/track_open_instances)
9
+ [![Slack](https://img.shields.io/badge/slack-main--branch/track__open__instances-yellow.svg?logo=slack)](https://main-branch.slack.com/archives/C01CHR7TMM2)
10
+
11
+ `TrackOpenInstances` is a Ruby mixin designed to help manage resources that require
12
+ explicit cleanup. It provides a simple mechanism to track instances of a
13
+ class from creation until they are explicitly closed or disposed of.
14
+
15
+ By including this module in classes that manage resources like files, network
16
+ connections, or temporary objects, you can easily monitor if any instances are left
17
+ open, potentially causing resource leaks. The module keeps an internal list
18
+ of all active instances and provides methods to check the count of open instances and
19
+ assert that no instances remain unclosed, which is particularly useful in test
20
+ suites. It also stores the call stack at the time of instance creation
21
+ to aid debugging.
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```Ruby
28
+ gem 'track_open_instances'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```Ruby
34
+ bundle install
35
+ ```
36
+
37
+ Or install it directly from the command line:
38
+
39
+ ```Ruby
40
+ gem install track_open_instances
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Include the `TrackOpenInstances` module in any class where you need to track
46
+ instances that require explicit cleanup.
47
+
48
+ Call `add_open_instance` when an instance is created. This is typically done in
49
+ `initialize`.
50
+
51
+ Call `remove_open_instance` when it's cleaned up. This is typically done in a `close`
52
+ method.
53
+
54
+ ```Ruby
55
+ require 'track_open_instances'
56
+
57
+ # Example class managing a resource
58
+ class ManagedResource
59
+ include TrackOpenInstances
60
+
61
+ attr_reader :id
62
+
63
+ def initialize(id)
64
+ @id = id
65
+ puts "Opening resource: #{id}"
66
+ # Register instance for tracking
67
+ self.class.add_open_instance(self) #
68
+ end
69
+
70
+ def close
71
+ puts "Closing resource: #{id}"
72
+ # Remove instance from tracking
73
+ self.class.remove_open_instance(self) #
74
+ end
75
+ end
76
+ ```
77
+
78
+ With that integration, you will be able to keep track of all open instances.
79
+
80
+ ```Ruby
81
+ # --- Basic Usage ---
82
+
83
+ res1 = ManagedResource.new('A')
84
+ res2 = ManagedResource.new('B')
85
+
86
+ puts "Open count: #{ManagedResource.open_instance_count}" #=> 2
87
+
88
+ res1.close #
89
+
90
+ puts "Open count: #{ManagedResource.open_instance_count}" #=> 1
91
+
92
+ # --- Checking for Leaks (e.g., in tests) ---
93
+
94
+ # Get a report of open instances
95
+ report = ManagedResource.open_instances_report
96
+ puts report if report #=>
97
+ # There is 1 open ManagedResource instance:
98
+ # - object_id=...
99
+ # Call stack when created:
100
+ # ... (caller stack lines) ...
101
+
102
+ # Assert no instances are open (raises RuntimeError if any are found)
103
+ begin
104
+ ManagedResource.assert_no_open_instances #
105
+ rescue RuntimeError => e
106
+ puts "Assertion failed: #{e.message}"
107
+ end
108
+ # Output: Assertion failed: There is 1 open ManagedResource instance:...
109
+
110
+ res2.close
111
+
112
+ # This assertion will now pass
113
+ ManagedResource.assert_no_open_instances #
114
+ puts "All resources closed successfully."
115
+ # Output: All resources closed successfully.
116
+ ```
117
+
118
+ ## Development
119
+
120
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
121
+ `bundle exec rake` to run tests, static analysis, and build the gem.
122
+
123
+ For experimentation, you can also run `bin/console` for an interactive (IRB) prompt that
124
+ automatically requires track_open_instances.
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/main-branch/track_open_instances>.
129
+
130
+ ### Commit message guidelines
131
+
132
+ All commit messages must follow the [Conventional Commits
133
+ standard](https://www.conventionalcommits.org/en/v1.0.0/). This helps us maintain a
134
+ clear and structured commit history, automate versioning, and generate changelogs
135
+ effectively.
136
+
137
+ To ensure compliance, this project includes:
138
+
139
+ - A git commit-msg hook that validates your commit messages before they are accepted.
140
+
141
+ To activate the hook, you must have node installed and run `npm install`.
142
+
143
+ - A GitHub Actions workflow that will enforce the Conventional Commit standard as
144
+ part of the continuous integration pipeline.
145
+
146
+ Any commit message that does not conform to the Conventional Commits standard will
147
+ cause the workflow to fail and not allow the PR to be merged.
148
+
149
+ ### Pull request guidelines
150
+
151
+ All pull requests must be merged using rebase merges. This ensures that commit
152
+ messages from the feature branch are preserved in the release branch, keeping the
153
+ history clean and meaningful.
data/RELEASING.md ADDED
@@ -0,0 +1,9 @@
1
+ # How to release a new track_open_instances gem
2
+
3
+ Run `create-github-release <release-type>` in the root of a clean working tree.
4
+
5
+ Where `release-type` is `major`, `minor`, or `patch` depending on the nature of the
6
+ changes. Refer to the labels on each PR since the last release to determine which
7
+ semver release type to specify.
8
+
9
+ Follow the directions that `create-github-release` after it prepares the release PR.
data/Rakefile ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Run the same tasks that the CI build will run'
4
+ if RUBY_PLATFORM == 'java'
5
+ task default: %w[spec rubocop bundle:audit build]
6
+ else
7
+ task default: %w[spec rubocop yard bundle:audit build]
8
+ end
9
+
10
+ # Bundler Audit
11
+
12
+ require 'bundler/audit/task'
13
+ Bundler::Audit::Task.new
14
+
15
+ # Bundler Gem Build
16
+
17
+ require 'bundler'
18
+ require 'bundler/gem_tasks'
19
+
20
+ # RSpec
21
+
22
+ require 'rspec/core/rake_task'
23
+
24
+ RSpec::Core::RakeTask.new
25
+
26
+ CLEAN << 'coverage'
27
+ CLEAN << '.rspec_status'
28
+ CLEAN << 'rspec-report.xml'
29
+
30
+ # Rubocop
31
+
32
+ require 'rubocop/rake_task'
33
+
34
+ RuboCop::RakeTask.new
35
+
36
+ # YARD
37
+
38
+ unless RUBY_PLATFORM == 'java'
39
+ # yard:build
40
+
41
+ require 'yard'
42
+
43
+ YARD::Rake::YardocTask.new('yard:build') do |t|
44
+ t.files = %w[lib/**/*.rb examples/**/*]
45
+ t.options = %w[--no-private]
46
+ t.stats_options = %w[--list-undoc]
47
+ end
48
+
49
+ CLEAN << '.yardoc'
50
+ CLEAN << 'doc'
51
+
52
+ # yard:audit
53
+
54
+ desc 'Run yardstick to show missing YARD doc elements'
55
+ task :'yard:audit' do
56
+ sh "yardstick 'lib/**/*.rb'"
57
+ end
58
+
59
+ # yard:coverage
60
+
61
+ require 'yardstick/rake/verify'
62
+
63
+ Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
64
+ verify.threshold = 100
65
+ end
66
+
67
+ # yard
68
+
69
+ desc 'Run all YARD tasks'
70
+ task yard: %i[yard:build yard:audit yard:coverage]
71
+ end
@@ -0,0 +1,30 @@
1
+ module.exports = {
2
+ extends: ["@commitlint/config-conventional"],
3
+ rules: {
4
+ "body-leading-blank": [1, "always"],
5
+ "body-max-line-length": [2, "always", 100],
6
+ "footer-leading-blank": [1, "always"],
7
+ "header-max-length": [2, "always", 100],
8
+ "subject-empty": [2, "never"],
9
+ "subject-full-stop": [2, "never", "."],
10
+ "type-case": [2, "always", "lower-case"],
11
+ "type-empty": [2, "never"],
12
+ "type-enum": [
13
+ 2,
14
+ "always",
15
+ [
16
+ "build",
17
+ "chore",
18
+ "ci",
19
+ "docs",
20
+ "feat",
21
+ "fix",
22
+ "perf",
23
+ "refactor",
24
+ "revert",
25
+ "style",
26
+ "test",
27
+ ],
28
+ ],
29
+ },
30
+ };
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrackOpenInstances
4
+ # The last released version of this gem
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'track_open_instances/version'
4
+
5
+ # Mixin to track instances of a class that require explicit cleanup
6
+ #
7
+ # This module provides a mechanism to track instances of classes that need
8
+ # to be explicitly closed or cleaned up. It maintains a list of all instances
9
+ # created and allows checking for any unclosed instances, which can help in
10
+ # identifying resource leaks.
11
+ #
12
+ # @example Basic Usage
13
+ # class ManagedFile
14
+ # include TrackOpenInstances
15
+ #
16
+ # attr_reader :path
17
+ #
18
+ # def initialize(path)
19
+ # @path = path
20
+ # # Simulate opening the file
21
+ # puts "Opening file: #{path}"
22
+ # # Register the instance for tracking
23
+ # self.class.add_instance(self)
24
+ # end
25
+ #
26
+ # # Implement the close logic specific to the resource
27
+ # def close
28
+ # # Simulate closing the file
29
+ # puts "Closing file: #{path}"
30
+ # # Remove the instance from tracking
31
+ # self.class.remove_instance(self)
32
+ # end
33
+ # end
34
+ #
35
+ # file1 = ManagedFile.new('/tmp/file1.txt')
36
+ # file2 = ManagedFile.new('/tmp/file2.txt')
37
+ #
38
+ # puts ManagedFile.unclosed_instances.count #=> 2
39
+ # puts ManagedFile.unclosed_instances.inspect #=> [#<ManagedFile:...>, #<ManagedFile:...>]
40
+ #
41
+ # file1.close
42
+ #
43
+ # puts ManagedFile.unclosed_instances.count #=> 1
44
+ # puts ManagedFile.unclosed_instances.inspect #=> [#<ManagedFile:...>]
45
+ #
46
+ # # In a test suite's teardown, you might use:
47
+ # # ManagedFile.assert_no_open_instances # This would raise if file2 wasn't closed
48
+ #
49
+ # file2.close
50
+ # ManagedFile.assert_no_open_instances # This will now pass
51
+ #
52
+ # @api public
53
+ module TrackOpenInstances
54
+ # Represents an open instance of a class that includes TrackOpenInstances
55
+ #
56
+ # @attr_reader [Object] instance The tracked instance
57
+ # @attr_reader [Array<String>] creation_stack The call stack at the time of instance creation
58
+ #
59
+ # This is useful for debugging and identifying where the instance was created.
60
+ #
61
+ # @api private
62
+ class OpenInstance
63
+ attr_reader :instance, :creation_stack
64
+
65
+ # Initializes a new OpenInstance
66
+ #
67
+ # @param instance [Object] The tracked instance
68
+ # @param creation_stack [Array<String>] The call stack at the time of instance creation
69
+ #
70
+ # @return [void]
71
+ #
72
+ # @api private
73
+ #
74
+ def initialize(instance, creation_stack)
75
+ @instance = instance
76
+ @creation_stack = creation_stack
77
+ end
78
+ end
79
+
80
+ # Ruby hook executed when this module is included in a base class
81
+ #
82
+ # Sets up the necessary class instance variables and extends the base
83
+ # class with ClassMethods.
84
+ #
85
+ # @param base [Class] The class including this module
86
+ #
87
+ # @return [void]
88
+ #
89
+ # @api private
90
+ def self.included(base)
91
+ base.extend(ClassMethods)
92
+ # @!attribute [rw] open_instances
93
+ # Internal storage for all tracked instances
94
+ # @return [Hash{Integer => OpenInstance}] The list of currently tracked instances
95
+ base.instance_variable_set(:@open_instances, {}.compare_by_identity)
96
+ # @!visibility private
97
+ # @!attribute [r] open_instances_mutex
98
+ # Mutex to ensure thread-safe access to the open_instances list
99
+ base.instance_variable_set(:@open_instances_mutex, Thread::Mutex.new)
100
+ end
101
+
102
+ # Contains class-level methods added to classes including TrackOpenInstances
103
+ module ClassMethods
104
+ # @!attribute [r] open_instances
105
+ #
106
+ # Direct access to the internal list of tracked instances
107
+ #
108
+ # Note: This returns all instances ever tracked unless explicitly removed.
109
+ # Use `unclosed_instances` for checking leaks. Direct use is uncommon.
110
+ #
111
+ # @example
112
+ # # Assuming MyResource includes TrackOpenInstances
113
+ # res1 = MyResource.new
114
+ # res2 = MyResource.new
115
+ # res1.close
116
+ # MyResource.open_instances #=> [#<MyResource... object_id=res2>] (after res1 removed)
117
+ #
118
+ # @return [Array<Object>] The raw list of currently tracked instances
119
+ #
120
+ # @api private
121
+ def open_instances
122
+ @open_instances_mutex.synchronize do
123
+ @open_instances.dup.freeze
124
+ end
125
+ end
126
+
127
+ # Adds an instance to the tracking list (thread-safe)
128
+ #
129
+ # Typically called automatically by the instance's `initialize` method.
130
+ #
131
+ # @param instance [Object] The instance to add
132
+ #
133
+ # @return [void]
134
+ #
135
+ # @api private
136
+ def add_open_instance(instance)
137
+ @open_instances_mutex.synchronize do
138
+ @open_instances[instance] = OpenInstance.new(instance, caller(3..))
139
+ end
140
+ end
141
+
142
+ # Removes an instance from the tracking list (thread-safe)
143
+ #
144
+ # Typically called automatically by the instance's `close` method.
145
+ #
146
+ # @param instance [Object] The instance to remove
147
+ #
148
+ # @return [void]
149
+ #
150
+ # @api private
151
+ def remove_open_instance(instance)
152
+ @open_instances_mutex.synchronize do
153
+ @open_instances.delete(instance)
154
+ end
155
+ end
156
+
157
+ # The number of currently open instances
158
+ #
159
+ # @example
160
+ # res1 = MyResource.new
161
+ # res2 = MyResource.new
162
+ # MyResource.open_instance_count #=> 1
163
+ #
164
+ # @return [Integer]
165
+ #
166
+ # @api public
167
+ #
168
+ def open_instance_count
169
+ @open_instances_mutex.synchronize do
170
+ @open_instances.size
171
+ end
172
+ end
173
+
174
+ # Generates a report string listing unclosed instances and their creation stacks
175
+ #
176
+ # Useful for debugging resource leaks. Returns nil if no instances are unclosed.
177
+ #
178
+ # @example
179
+ # res = MyResource.new
180
+ # puts MyResource.open_instances_report
181
+ # There is 1 open MyResource instance(s):
182
+ # - object_id=701
183
+ # Created at:
184
+ # (caller stack line 1)
185
+ # (caller stack line 2)\n..."
186
+ #
187
+ # @return [String, nil] A formatted report string or nil if none are open
188
+ #
189
+ # @api public
190
+ def open_instances_report
191
+ @open_instances_mutex.synchronize do
192
+ return nil if @open_instances.count.zero?
193
+
194
+ String.new.tap do |report|
195
+ report << open_instances_report_header
196
+ report << open_instances_report_body
197
+ end
198
+ end
199
+ end
200
+
201
+ # The header string for the report
202
+ #
203
+ # @return [String]
204
+ #
205
+ # @api private
206
+ #
207
+ def open_instances_report_header
208
+ count = @open_instances.count
209
+
210
+ "There #{count == 1 ? 'is' : 'are'} #{count} " \
211
+ "open #{self.class} instance#{count == 1 ? '' : 's'}:\n"
212
+ end
213
+
214
+ # The body of the report detailing each open instance
215
+ #
216
+ # @return [String]
217
+ #
218
+ # @api private
219
+ #
220
+ def open_instances_report_body
221
+ String.new.tap do |body|
222
+ @open_instances.each do |instance, open_instance|
223
+ body << " - object_id=#{instance.object_id}\n"
224
+ body << " Call stack when created:\n"
225
+ open_instance.creation_stack.reverse.each { |line| body << " #{line}\n" }
226
+ end
227
+ end
228
+ end
229
+
230
+ # Asserts that no instances of the class remain unclosed
231
+ #
232
+ # Raises a ProcessExecuter::Error with a detailed report if any instances
233
+ # are found to be unclosed. Commonly used in test suite teardown blocks.
234
+ #
235
+ # @example
236
+ # # In RSpec teardown (e.g., after(:each))
237
+ # MyResource.assert_no_open_instances
238
+ #
239
+ # @raise [RuntimeError] If any instances are found unclosed
240
+ #
241
+ # @return [void]
242
+ #
243
+ # @api public
244
+ def assert_no_open_instances
245
+ report = open_instances_report
246
+ raise(report.to_s) if report
247
+ end
248
+ end
249
+ end
data/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "devDependencies": {
3
+ "@commitlint/cli": "^19.5.0",
4
+ "@commitlint/config-conventional": "^19.5.0",
5
+ "husky": "^9.1.0"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "husky",
9
+ "prepare": "husky"
10
+ }
11
+ }
data/pre-commit ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+
3
+ ##
4
+ # Run this before doing a commit. To ensure this happens, after cloning this
5
+ # repository, run bin/setup from the projects root directory
6
+
7
+ echo 'Performing pre-commit checks'
8
+
9
+ # Make sure we are testing with the latest version of gems
10
+ # since this is what will get installed in the CI build
11
+ #
12
+ echo "Running 'bundle update'..."
13
+ if ! bundle update > /dev/null; then
14
+ echo 'FAIL: bundle update failed'
15
+ exit 1
16
+ fi
17
+
18
+ # Run all the tests, code/doc analysis, gem build, etc.
19
+ #
20
+ echo "Running 'bundle exec rake default'..."
21
+ if ! bundle exec rake default > /dev/null 2>&1; then
22
+ echo 'FAIL: Rake default task failed'
23
+ exit 1
24
+ fi
25
+
26
+ echo 'SUCCESS'
metadata ADDED
@@ -0,0 +1,243 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: track_open_instances
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Couball
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-04-08 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler-audit
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: create_github_release
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: main_branch_shared_rubocop_config
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.13'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.13'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.75'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.75'
96
+ - !ruby/object:Gem::Dependency
97
+ name: simplecov
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.22'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.22'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov-json
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.2'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.2'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov-rspec
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.4'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.4'
138
+ - !ruby/object:Gem::Dependency
139
+ name: redcarpet
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.6'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.6'
152
+ - !ruby/object:Gem::Dependency
153
+ name: yard
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '0.9'
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 0.9.28
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '0.9'
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: 0.9.28
172
+ - !ruby/object:Gem::Dependency
173
+ name: yardstick
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '0.9'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '0.9'
186
+ description: |
187
+ A mixin to track instances of Ruby classes that require explicit cleanup,
188
+ helping to identify potential resource leaks. It maintains a list of
189
+ created instances and allows checking for any that remain unclosed.
190
+ email:
191
+ - jcouball@yahoo.com
192
+ executables: []
193
+ extensions: []
194
+ extra_rdoc_files: []
195
+ files:
196
+ - ".husky/commit-msg"
197
+ - ".markdownlint.yml"
198
+ - ".qlty/qlty.toml"
199
+ - ".rspec"
200
+ - ".rubocop.yml"
201
+ - ".yamllint.yml"
202
+ - ".yardopts"
203
+ - CHANGELOG.md
204
+ - CONTRIBUTING.md
205
+ - LICENSE.txt
206
+ - README.md
207
+ - RELEASING.md
208
+ - Rakefile
209
+ - commitlint.config.js
210
+ - lib/track_open_instances.rb
211
+ - lib/track_open_instances/version.rb
212
+ - package.json
213
+ - pre-commit
214
+ homepage: https://github.com/main-branch/track_open_instances
215
+ licenses:
216
+ - MIT
217
+ metadata:
218
+ allowed_push_host: https://rubygems.org
219
+ homepage_uri: https://github.com/main-branch/track_open_instances
220
+ source_code_uri: https://github.com/main-branch/track_open_instances
221
+ documentation_uri: https://rubydoc.info/gems/track_open_instances/0.1.0
222
+ changelog_uri: https://rubydoc.info/gems/track_open_instances/0.1.0/file/CHANGELOG.md
223
+ rubygems_mfa_required: 'true'
224
+ rdoc_options: []
225
+ require_paths:
226
+ - lib
227
+ required_ruby_version: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - ">="
230
+ - !ruby/object:Gem::Version
231
+ version: 3.1.0
232
+ required_rubygems_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ requirements:
238
+ - 'Platform: Mac, Linux, or Windows'
239
+ - 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
240
+ rubygems_version: 3.6.2
241
+ specification_version: 4
242
+ summary: A mixin to ensure that all instances of a class are closed
243
+ test_files: []