1use crate::config::DepsConfig;
9use crate::document::{ServerState, ensure_document_loaded};
10use deps_core::VersionData;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tower_lsp_server::Client;
14use tower_lsp_server::ls_types::{CodeLens, CodeLensParams};
15
16pub const COMMAND_ID: &str = "deps-lsp.updateAllOutdated";
18
19pub async fn handle_code_lens(
30 state: Arc<ServerState>,
31 params: CodeLensParams,
32 enabled: bool,
33 client: Client,
34 config: Arc<RwLock<DepsConfig>>,
35) -> Vec<CodeLens> {
36 if !enabled {
37 return vec![];
38 }
39
40 let uri = ¶ms.text_document.uri;
41
42 if !ensure_document_loaded(uri, Arc::clone(&state), client, Arc::clone(&config)).await {
44 tracing::warn!("Could not load document for code lens: {:?}", uri);
45 return vec![];
46 }
47
48 let offline = { config.read().await.network.offline };
49
50 let Some((ecosystem, parse_result, content, cached_versions, resolved_versions)) = state
54 .with_document(uri, |doc| {
55 let ecosystem = state.ecosystem_registry.get(doc.ecosystem_id())?;
56
57 if !doc.is_ready_for_batch_update() {
64 return None;
65 }
66
67 let parse_result = doc.parse_result_arc()?;
68 Some((
69 ecosystem,
70 parse_result,
71 doc.content.clone(),
72 doc.cached_versions.clone(),
73 doc.resolved_versions.clone(),
74 ))
75 })
76 .flatten()
77 else {
78 return vec![];
79 };
80
81 ecosystem
82 .generate_code_lenses(
83 parse_result.as_ref(),
84 &content,
85 VersionData::new(&cached_versions, &resolved_versions).with_offline(offline),
86 uri,
87 COMMAND_ID,
88 )
89 .await
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::document::ServerState;
96 use crate::test_utils::test_helpers::create_test_client_and_config;
97 use deps_core::{EcosystemId, PackageVersions};
98 use tower_lsp_server::ls_types::TextDocumentIdentifier;
99
100 fn params(uri: tower_lsp_server::ls_types::Uri) -> CodeLensParams {
101 CodeLensParams {
102 text_document: TextDocumentIdentifier { uri },
103 work_done_progress_params: Default::default(),
104 partial_result_params: Default::default(),
105 }
106 }
107
108 #[tokio::test]
109 async fn test_handle_code_lens_disabled_returns_empty() {
110 let state = Arc::new(ServerState::new());
111 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
112 let (client, config) = create_test_client_and_config();
113
114 let result = handle_code_lens(state, params(uri), false, client, config).await;
115 assert!(result.is_empty());
116 }
117
118 #[tokio::test]
119 async fn test_handle_code_lens_missing_document_returns_empty() {
120 let state = Arc::new(ServerState::new());
121 let uri = deps_core::test_util::test_uri("/test/unknown.txt");
122 let (client, config) = create_test_client_and_config();
123
124 let result = handle_code_lens(state, params(uri), true, client, config).await;
125 assert!(result.is_empty());
126 }
127
128 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
140 async fn test_concurrent_document_write_not_blocked_by_in_flight_code_lens() {
141 use crate::document::DocumentState;
142 use crate::test_utils::blocking_ecosystem::{
143 BlockingEcosystem, BlockingHook, MockParseResult,
144 };
145 use deps_core::ParseResult;
146 use tokio::sync::Barrier;
147
148 let state = Arc::new(ServerState::new());
149 let started = Arc::new(Barrier::new(2));
150 state
151 .ecosystem_registry
152 .register(Arc::new(BlockingEcosystem {
153 started: Arc::clone(&started),
154 hook: BlockingHook::CodeLenses,
155 }));
156
157 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
158 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
159 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
160 let mut doc =
161 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
162 doc.set_version(Some(1));
163 state.update_document(uri.clone(), doc);
164
165 let (client, config) = create_test_client_and_config();
166
167 let handler_task = tokio::spawn({
168 let state = Arc::clone(&state);
169 let uri = uri.clone();
170 async move { handle_code_lens(state, params(uri), true, client, config).await }
171 });
172
173 tokio::time::timeout(std::time::Duration::from_secs(5), started.wait())
178 .await
179 .expect("handle_code_lens did not reach generate_code_lenses within 5s");
180
181 let write_task = tokio::spawn({
185 let state = Arc::clone(&state);
186 let uri = uri.clone();
187 async move {
188 state.documents.get_mut(&uri).unwrap().set_loading();
189 }
190 });
191 let write_result =
192 tokio::time::timeout(std::time::Duration::from_millis(500), write_task).await;
193
194 handler_task.abort();
195
196 assert!(
197 write_result.is_ok(),
198 "#333 regression: a concurrent documents.get_mut on the same URI must not \
199 block on an in-flight generate_code_lenses call — the DashMap shard Ref \
200 must be dropped before the call is awaited, not after it"
201 );
202 }
203
204 #[cfg(feature = "cargo")]
205 mod cargo_tests {
206 use super::*;
207 use crate::document::DocumentState;
208
209 async fn seed(
210 state: &Arc<ServerState>,
211 uri: &tower_lsp_server::ls_types::Uri,
212 content: &str,
213 cached: std::collections::HashMap<deps_core::PackageName, deps_core::PackageVersions>,
214 ) {
215 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
216 let parse_result = ecosystem
217 .parse_manifest(content, uri)
218 .await
219 .expect("failed to parse manifest");
220 let mut doc_state = DocumentState::new_from_parse_result(
221 EcosystemId::Cargo,
222 content.to_string(),
223 parse_result,
224 );
225 doc_state.update_cached_versions(cached);
226 doc_state.set_loaded();
227 doc_state.set_version(Some(1));
228 state.update_document(uri.clone(), doc_state);
229 }
230
231 #[tokio::test]
232 async fn test_handle_code_lens_no_parse_result_returns_empty() {
233 let state = Arc::new(ServerState::new());
234 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
235 let doc_state =
236 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
237 state.update_document(uri.clone(), doc_state);
238
239 let (client, config) = create_test_client_and_config();
240 let result = handle_code_lens(state, params(uri), true, client, config).await;
241 assert!(result.is_empty());
242 }
243
244 #[tokio::test]
245 async fn test_handle_code_lens_loading_returns_empty() {
246 let state = Arc::new(ServerState::new());
247 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
248 let content = "[dependencies]\nserde = \"1.0.0\"\n";
249 let mut cached = std::collections::HashMap::new();
250 cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
251 seed(&state, &uri, content, cached).await;
252 state.documents.get_mut(&uri).unwrap().set_loading();
253
254 let (client, config) = create_test_client_and_config();
255 let result = handle_code_lens(state, params(uri), true, client, config).await;
256 assert!(result.is_empty());
257 }
258
259 #[tokio::test]
260 async fn test_handle_code_lens_no_version_returns_empty() {
261 let state = Arc::new(ServerState::new());
265 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
266 let content = "[dependencies]\nserde = \"1.0.0\"\n";
267 let mut cached = std::collections::HashMap::new();
268 cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
269 seed(&state, &uri, content, cached).await;
270 state.documents.get_mut(&uri).unwrap().set_version(None);
271
272 let (client, config) = create_test_client_and_config();
273 let result = handle_code_lens(state, params(uri), true, client, config).await;
274 assert!(result.is_empty());
275 }
276
277 #[tokio::test]
278 async fn test_handle_code_lens_up_to_date_fixture_returns_no_lens() {
279 let state = Arc::new(ServerState::new());
280 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
281 let content = "[dependencies]\nserde = \"1.0.0\"\n";
282 let mut cached = std::collections::HashMap::new();
283 cached.insert("serde".into(), PackageVersions::latest_only("1.0.0"));
284 seed(&state, &uri, content, cached).await;
285
286 let (client, config) = create_test_client_and_config();
287 let result = handle_code_lens(state, params(uri), true, client, config).await;
288 assert!(result.is_empty());
289 }
290
291 #[tokio::test]
292 async fn test_handle_code_lens_outdated_fixture_returns_one_lens() {
293 let state = Arc::new(ServerState::new());
294 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
295 let content = "[dependencies]\nserde = \"1.0.0\"\n";
296 let mut cached = std::collections::HashMap::new();
297 cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
298 seed(&state, &uri, content, cached).await;
299
300 let (client, config) = create_test_client_and_config();
301 let result = handle_code_lens(state, params(uri.clone()), true, client, config).await;
302
303 assert_eq!(result.len(), 1);
304 let command = result[0].command.as_ref().expect("lens has a command");
305 assert_eq!(command.title, "Update 1 outdated dependency");
306 assert_eq!(command.command, COMMAND_ID);
307 let args = command
308 .arguments
309 .as_ref()
310 .expect("command has arguments")
311 .first()
312 .expect("command has one argument");
313 assert_eq!(args["uri"], uri.as_str());
314 }
315 }
316
317 mod cross_ecosystem_tests {
325 use super::*;
326 use std::collections::HashMap;
327 use tower_lsp_server::ls_types::TextEdit;
328
329 fn apply_single_edit(content: &str, edit: &TextEdit) -> String {
333 let table = deps_core::LineOffsetTable::new(content);
334 let start = table.position_to_byte_offset(content, edit.range.start);
335 let end = table.position_to_byte_offset(content, edit.range.end);
336 format!("{}{}{}", &content[..start], edit.new_text, &content[end..])
337 }
338
339 async fn assert_single_edit_produces_valid_declaration(
343 ecosystem: &dyn deps_core::Ecosystem,
344 uri: &tower_lsp_server::ls_types::Uri,
345 content: &str,
346 cached: HashMap<deps_core::PackageName, deps_core::PackageVersions>,
347 expected_fragment: &str,
348 ) {
349 let parse_result = ecosystem
350 .parse_manifest(content, uri)
351 .await
352 .expect("fixture must parse");
353 let resolved = HashMap::new();
354 let edits = deps_core::collect_update_all_edits(
355 parse_result.as_ref(),
356 content,
357 deps_core::VersionData::new(&cached, &resolved),
358 ecosystem.formatter(),
359 );
360 assert_eq!(edits.len(), 1, "expected exactly one edit for this fixture");
361
362 let new_content = apply_single_edit(content, &edits[0]);
363 assert!(
364 new_content.contains(expected_fragment),
365 "resulting text should contain {expected_fragment:?}, got: {new_content}"
366 );
367 ecosystem
368 .parse_manifest(&new_content, uri)
369 .await
370 .unwrap_or_else(|e| panic!("resulting text failed to re-parse: {e}"));
371 }
372
373 async fn assert_guard_skips(
375 ecosystem: &dyn deps_core::Ecosystem,
376 uri: &tower_lsp_server::ls_types::Uri,
377 content: &str,
378 cached: HashMap<deps_core::PackageName, deps_core::PackageVersions>,
379 ) {
380 let parse_result = ecosystem
381 .parse_manifest(content, uri)
382 .await
383 .expect("fixture must parse");
384 let resolved = HashMap::new();
385 let edits = deps_core::collect_update_all_edits(
386 parse_result.as_ref(),
387 content,
388 deps_core::VersionData::new(&cached, &resolved),
389 ecosystem.formatter(),
390 );
391 assert!(
392 edits.is_empty(),
393 "expected the literal-span guard to skip this dependency"
394 );
395 }
396
397 #[cfg(feature = "cargo")]
398 #[tokio::test]
399 async fn test_cargo_literal_version_is_edited() {
400 let state = ServerState::new();
401 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
402 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
403 let content = "[dependencies]\nserde = \"1.0.0\"\n";
404 let mut cached = HashMap::new();
405 cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
406
407 assert_single_edit_produces_valid_declaration(
408 ecosystem.as_ref(),
409 &uri,
410 content,
411 cached,
412 "serde = \"1.2.0\"",
413 )
414 .await;
415 }
416
417 #[cfg(feature = "npm")]
418 #[tokio::test]
419 async fn test_npm_literal_version_is_edited() {
420 let state = ServerState::new();
421 let ecosystem = state.ecosystem_registry.get("npm").unwrap();
422 let uri = deps_core::test_util::test_uri("/test/package.json");
423 let content = r#"{"dependencies": {"express": "^4.0.0"}}"#;
424 let mut cached = HashMap::new();
425 cached.insert("express".into(), PackageVersions::latest_only("5.0.0"));
426
427 assert_single_edit_produces_valid_declaration(
428 ecosystem.as_ref(),
429 &uri,
430 content,
431 cached,
432 "\"express\": \"5.0.0\"",
433 )
434 .await;
435 }
436
437 #[cfg(feature = "pypi")]
438 #[tokio::test]
439 async fn test_pypi_literal_version_is_edited() {
440 let state = ServerState::new();
441 let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
442 let uri = deps_core::test_util::test_uri("/test/pyproject.toml");
443 let content = "[project]\ndependencies = [\"requests==2.0.0\"]\n";
448 let mut cached = HashMap::new();
449 cached.insert("requests".into(), PackageVersions::latest_only("2.5.0"));
450
451 assert_single_edit_produces_valid_declaration(
452 ecosystem.as_ref(),
453 &uri,
454 content,
455 cached,
456 "requests==2.5.0",
457 )
458 .await;
459 }
460
461 #[cfg(feature = "go")]
462 #[tokio::test]
463 async fn test_go_literal_version_is_edited() {
464 let state = ServerState::new();
465 let ecosystem = state.ecosystem_registry.get("go").unwrap();
466 let uri = deps_core::test_util::test_uri("/test/go.mod");
467 let content =
468 "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n";
469 let mut cached = HashMap::new();
470 cached.insert(
471 "github.com/gin-gonic/gin".into(),
472 PackageVersions::latest_only("v1.10.0"),
473 );
474
475 assert_single_edit_produces_valid_declaration(
476 ecosystem.as_ref(),
477 &uri,
478 content,
479 cached,
480 "require github.com/gin-gonic/gin v1.10.0",
481 )
482 .await;
483 }
484
485 #[cfg(feature = "dart")]
486 #[tokio::test]
487 async fn test_dart_literal_version_is_edited() {
488 let state = ServerState::new();
489 let ecosystem = state.ecosystem_registry.get("dart").unwrap();
490 let uri = deps_core::test_util::test_uri("/test/pubspec.yaml");
491 let content = "dependencies:\n http: ^1.0.0\n";
493 let mut cached = HashMap::new();
494 cached.insert("http".into(), PackageVersions::latest_only("2.0.0"));
495
496 assert_single_edit_produces_valid_declaration(
497 ecosystem.as_ref(),
498 &uri,
499 content,
500 cached,
501 "http: ^2.0.0",
502 )
503 .await;
504 }
505
506 #[cfg(feature = "nuget")]
507 #[tokio::test]
508 async fn test_nuget_literal_version_is_edited() {
509 let state = ServerState::new();
510 let ecosystem = state.ecosystem_registry.get("nuget").unwrap();
511 let uri = deps_core::test_util::test_uri("/test/project.csproj");
512 let content = r#"<Project><ItemGroup><PackageReference Include="Newtonsoft.Json" Version="12.0.3" /></ItemGroup></Project>"#;
513 let mut cached = HashMap::new();
514 cached.insert(
515 "Newtonsoft.Json".into(),
516 PackageVersions::latest_only("13.0.3"),
517 );
518
519 assert_single_edit_produces_valid_declaration(
520 ecosystem.as_ref(),
521 &uri,
522 content,
523 cached,
524 r#"Version="13.0.3""#,
525 )
526 .await;
527 }
528
529 #[cfg(feature = "composer")]
530 #[tokio::test]
531 async fn test_composer_literal_version_is_edited() {
532 let state = ServerState::new();
533 let ecosystem = state.ecosystem_registry.get("composer").unwrap();
534 let uri = deps_core::test_util::test_uri("/test/composer.json");
535 let content = "{\n \"require\": {\n \"symfony/console\": \"^6.0\"\n }\n}";
536 let mut cached = HashMap::new();
537 cached.insert(
538 "symfony/console".into(),
539 PackageVersions::latest_only("7.0.0"),
540 );
541
542 assert_single_edit_produces_valid_declaration(
543 ecosystem.as_ref(),
544 &uri,
545 content,
546 cached,
547 "\"symfony/console\": \"7.0.0\"",
548 )
549 .await;
550 }
551
552 #[cfg(feature = "bundler")]
553 #[tokio::test]
554 async fn test_bundler_literal_version_is_edited() {
555 let state = ServerState::new();
556 let ecosystem = state.ecosystem_registry.get("bundler").unwrap();
557 let uri = deps_core::test_util::test_uri("/test/Gemfile");
558 let content = "source 'https://rubygems.org'\ngem 'rails', '~> 7.0'";
559 let mut cached = HashMap::new();
560 cached.insert("rails".into(), PackageVersions::latest_only("8.0.0"));
561
562 assert_single_edit_produces_valid_declaration(
563 ecosystem.as_ref(),
564 &uri,
565 content,
566 cached,
567 "gem 'rails', '8.0.0'",
568 )
569 .await;
570 }
571
572 #[cfg(feature = "maven")]
573 #[tokio::test]
574 async fn test_maven_literal_version_is_edited() {
575 let state = ServerState::new();
576 let ecosystem = state.ecosystem_registry.get("maven").unwrap();
577 let uri = deps_core::test_util::test_uri("/test/pom.xml");
578 let content = r"<project>
579 <dependencies>
580 <dependency>
581 <groupId>org.apache.commons</groupId>
582 <artifactId>commons-lang3</artifactId>
583 <version>3.12.0</version>
584 </dependency>
585 </dependencies>
586</project>
587";
588 let mut cached = HashMap::new();
589 cached.insert(
590 "org.apache.commons:commons-lang3".into(),
591 PackageVersions::latest_only("3.14.0"),
592 );
593
594 assert_single_edit_produces_valid_declaration(
595 ecosystem.as_ref(),
596 &uri,
597 content,
598 cached,
599 "<version>3.14.0</version>",
600 )
601 .await;
602 }
603
604 #[cfg(feature = "maven")]
605 #[tokio::test]
606 async fn test_maven_property_version_is_skipped() {
607 let state = ServerState::new();
608 let ecosystem = state.ecosystem_registry.get("maven").unwrap();
609 let uri = deps_core::test_util::test_uri("/test/pom.xml");
610 let content = r"<project>
611 <properties>
612 <slf4j.version>2.0.16</slf4j.version>
613 </properties>
614 <dependencies>
615 <dependency>
616 <groupId>org.slf4j</groupId>
617 <artifactId>slf4j-api</artifactId>
618 <version>${slf4j.version}</version>
619 </dependency>
620 </dependencies>
621</project>
622";
623 let mut cached = HashMap::new();
624 cached.insert(
625 "org.slf4j:slf4j-api".into(),
626 PackageVersions::latest_only("2.1.0"),
627 );
628
629 assert_guard_skips(ecosystem.as_ref(), &uri, content, cached).await;
630 }
631
632 #[cfg(feature = "gradle")]
633 #[tokio::test]
634 async fn test_gradle_dsl_variable_is_skipped() {
635 let temp_dir = tempfile::TempDir::new().unwrap();
640 let build_gradle_path = temp_dir.path().join("build.gradle");
641 let content = "dependencies {\n implementation \"org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion\"\n}\n";
642 std::fs::write(&build_gradle_path, content).unwrap();
643 std::fs::write(
644 temp_dir.path().join("gradle.properties"),
645 "kotlinVersion=2.1.10\n",
646 )
647 .unwrap();
648
649 let uri = tower_lsp_server::ls_types::Uri::from_file_path(&build_gradle_path).unwrap();
650 let state = ServerState::new();
651 let ecosystem = state.ecosystem_registry.get("gradle").unwrap();
652
653 let mut cached = HashMap::new();
654 cached.insert(
655 "org.jetbrains.kotlin:kotlin-stdlib".into(),
656 PackageVersions::latest_only("2.2.0"),
657 );
658
659 assert_guard_skips(ecosystem.as_ref(), &uri, content, cached).await;
660 }
661
662 #[cfg(feature = "gradle")]
663 #[tokio::test]
664 async fn test_gradle_version_catalog_alias_is_skipped() {
665 let state = ServerState::new();
666 let ecosystem = state.ecosystem_registry.get("gradle").unwrap();
667 let uri = deps_core::test_util::test_uri("/test/gradle/libs.versions.toml");
668 let content = "[versions]\nspring = \"3.2.0\"\n\n[libraries]\nspring-boot = { module = \"org.springframework.boot:spring-boot-starter\", version.ref = \"spring\" }\n";
669 let mut cached = HashMap::new();
670 cached.insert(
671 "org.springframework.boot:spring-boot-starter".into(),
672 PackageVersions::latest_only("3.3.0"),
673 );
674
675 assert_guard_skips(ecosystem.as_ref(), &uri, content, cached).await;
676 }
677
678 #[cfg(feature = "swift")]
679 #[tokio::test]
680 async fn test_swift_from_form_is_edited() {
681 let state = ServerState::new();
687 let ecosystem = state.ecosystem_registry.get("swift").unwrap();
688 let uri = deps_core::test_util::test_uri("/test/Package.swift");
689 let content = r#"
690let package = Package(
691 dependencies: [
692 .package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0"),
693 ]
694)
695"#;
696 let mut cached = HashMap::new();
697 cached.insert(
698 "apple/swift-nio".into(),
699 PackageVersions::latest_only("3.0.0"),
700 );
701
702 assert_single_edit_produces_valid_declaration(
703 ecosystem.as_ref(),
704 &uri,
705 content,
706 cached,
707 "3.0.0",
708 )
709 .await;
710 }
711
712 #[cfg(feature = "swift")]
713 #[tokio::test]
714 async fn test_swift_range_forms_are_still_skipped() {
715 let state = ServerState::new();
723 let ecosystem = state.ecosystem_registry.get("swift").unwrap();
724 let uri = deps_core::test_util::test_uri("/test/Package.swift");
725
726 for content in [
727 r#".package(url: "https://github.com/foo/bar", "1.0.0"..<"2.0.0")"#,
728 r#".package(url: "https://github.com/baz/qux", "1.0.0"..."1.9.9")"#,
729 ] {
730 let mut cached = HashMap::new();
731 cached.insert("foo/bar".into(), PackageVersions::latest_only("3.5.0"));
732 cached.insert("baz/qux".into(), PackageVersions::latest_only("3.5.0"));
733
734 assert_guard_skips(ecosystem.as_ref(), &uri, content, cached).await;
735 }
736 }
737
738 #[cfg(feature = "github-actions")]
745 fn seed_gha_tag_index(
746 ecosystem: &dyn deps_core::Ecosystem,
747 name: &str,
748 tag: &str,
749 sha: &str,
750 ) {
751 let registry = ecosystem.registry();
752 let gha_registry = registry
753 .as_any()
754 .downcast_ref::<deps_github_actions::GithubActionsRegistry>()
755 .expect("github-actions ecosystem must back onto a GithubActionsRegistry");
756 let mut index = deps_github_actions::registry::TagIndex::default();
757 index.tag_to_sha.insert(tag.to_string(), sha.to_string());
758 index.sha_to_tag.insert(sha.to_string(), tag.to_string());
759 gha_registry.tag_index().insert(
760 deps_core::PackageName::new(name),
761 std::sync::Arc::new(index),
762 );
763 }
764
765 #[cfg(feature = "github-actions")]
766 #[tokio::test]
767 async fn test_github_actions_tag_pin_is_edited() {
768 let state = ServerState::new();
769 let ecosystem = state.ecosystem_registry.get("github-actions").unwrap();
770 let uri = deps_core::test_util::test_uri("/repo/.github/workflows/ci.yml");
771 let content = "steps:\n - uses: actions/checkout@v4.2.0\n";
772 let mut cached = HashMap::new();
773 cached.insert(
774 "actions/checkout".into(),
775 PackageVersions::latest_only("v4.3.0"),
776 );
777
778 assert_single_edit_produces_valid_declaration(
779 ecosystem.as_ref(),
780 &uri,
781 content,
782 cached,
783 "v4.3.0",
784 )
785 .await;
786 }
787
788 #[cfg(feature = "github-actions")]
789 #[tokio::test]
790 async fn test_github_actions_sha_with_comment_pin_is_edited_to_new_sha_and_tag() {
791 let state = ServerState::new();
792 let ecosystem = state.ecosystem_registry.get("github-actions").unwrap();
793 let uri = deps_core::test_util::test_uri("/repo/.github/workflows/ci.yml");
794 let old_sha = "a".repeat(40);
795 let new_sha = "b".repeat(40);
796 seed_gha_tag_index(ecosystem.as_ref(), "actions/checkout", "v4.3.0", &new_sha);
797
798 let content = format!("steps:\n - uses: actions/checkout@{old_sha} # v4.2.0\n");
799 let mut cached = HashMap::new();
800 cached.insert(
801 "actions/checkout".into(),
802 PackageVersions::latest_only("v4.3.0"),
803 );
804
805 assert_single_edit_produces_valid_declaration(
806 ecosystem.as_ref(),
807 &uri,
808 &content,
809 cached,
810 &format!("{new_sha} # v4.3.0"),
811 )
812 .await;
813 }
814
815 #[cfg(feature = "github-actions")]
816 #[tokio::test]
817 async fn test_github_actions_subdirectory_action_sha_pin_is_edited_preserving_subpath() {
818 let state = ServerState::new();
824 let ecosystem = state.ecosystem_registry.get("github-actions").unwrap();
825 let uri = deps_core::test_util::test_uri("/repo/.github/workflows/ci.yml");
826 let old_sha = "a".repeat(40);
827 let new_sha = "b".repeat(40);
828 seed_gha_tag_index(
829 ecosystem.as_ref(),
830 "github/codeql-action",
831 "v3.1.0",
832 &new_sha,
833 );
834
835 let content =
836 format!("steps:\n - uses: github/codeql-action/init@{old_sha} # v3.0.0\n");
837 let mut cached = HashMap::new();
838 cached.insert(
839 "github/codeql-action".into(),
840 PackageVersions::latest_only("v3.1.0"),
841 );
842
843 assert_single_edit_produces_valid_declaration(
844 ecosystem.as_ref(),
845 &uri,
846 &content,
847 cached,
848 &format!("codeql-action/init@{new_sha} # v3.1.0"),
849 )
850 .await;
851 }
852
853 #[cfg(feature = "github-actions")]
854 #[tokio::test]
855 async fn test_github_actions_sha_pin_tag_index_miss_is_skipped() {
856 let state = ServerState::new();
861 let ecosystem = state.ecosystem_registry.get("github-actions").unwrap();
862 let uri = deps_core::test_util::test_uri("/repo/.github/workflows/ci.yml");
863 let old_sha = "a".repeat(40);
864 let content = format!("steps:\n - uses: actions/checkout@{old_sha} # v4.2.0\n");
868 let mut cached = HashMap::new();
869 cached.insert(
870 "actions/checkout".into(),
871 PackageVersions::latest_only("v4.3.0"),
872 );
873
874 assert_guard_skips(ecosystem.as_ref(), &uri, &content, cached).await;
875 }
876 }
877}