Skip to main content

perforce_cli/cmd/
attribute.rs

1use std::ffi::OsStr;
2use std::io::Write;
3#[cfg(not(feature = "lt2024_2"))]
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::{Child, Command, Output, Stdio};
7
8use super::{ExclusiveOption, SubCommand, Unselected};
9
10use crate::global::GlobalOpts;
11use crate::spawn::ParameterizedSpawn;
12
13/// Default value source of `p4 attribute`: set or clear attributes with
14/// `-n name [-v value]` pairs.
15///
16/// Each entry is an attribute name together with the value to set, or `None`
17/// to clear the attribute by omitting `-v`.
18#[derive(Debug, Clone, Default)]
19pub struct Standard {
20    values: Option<Vec<(String, Option<String>)>>,
21
22    hex: bool,
23}
24
25impl ExclusiveOption for Standard {
26    fn inject_args(&self, command: &mut Command) {
27        if self.hex {
28            command.arg("-e");
29        }
30
31        if let Some(pairs) = &self.values {
32            for (name, value) in pairs {
33                command.arg("-n").arg(name);
34
35                if let Some(value) = value {
36                    command.arg("-v").arg(value);
37                }
38            }
39        }
40    }
41}
42
43/// Read the attribute value from standard input (`-i`).
44///
45/// Entered with [`Attribute::read_from_stdin`]; only one file argument is
46/// allowed in this state.
47#[derive(Debug, Clone, Default)]
48pub struct FromStdin {
49    hex: bool,
50
51    name: String,
52}
53
54impl ExclusiveOption for FromStdin {
55    fn inject_args(&self, command: &mut Command) {
56        if self.hex {
57            command.arg("-e");
58        }
59
60        command.arg("-i").arg("-n").arg(&self.name);
61    }
62}
63
64/// Read the attribute value from a file (`-I filename`).
65///
66/// Entered with [`Attribute::read_from_file`]; `-e` is not available and only
67/// one file argument is allowed in this state.
68#[cfg(not(feature = "lt2024_2"))]
69#[derive(Debug, Clone, Default)]
70pub struct FromFile {
71    name: String,
72
73    file: PathBuf,
74}
75
76#[cfg(not(feature = "lt2024_2"))]
77impl ExclusiveOption for FromFile {
78    fn inject_args(&self, command: &mut Command) {
79        command.arg("-I").arg(&self.file).arg("-n").arg(&self.name);
80    }
81}
82
83/// Trait storage location of the `-T0` / `-T1` option group of
84/// `p4 attribute`.
85#[cfg(not(feature = "lt2023_2"))]
86pub mod storage {
87    /// `-T0`: store the attribute value in the `db.traits` table.
88    #[derive(Debug, Clone, Copy, Default)]
89    pub struct DatabaseTraits;
90
91    /// `-T1`: store the attribute value in the `trait` depot.
92    #[derive(Debug, Clone, Copy, Default)]
93    pub struct TraitDepot;
94}
95
96#[cfg(not(feature = "lt2023_2"))]
97impl ExclusiveOption for storage::DatabaseTraits {
98    fn inject_args(&self, command: &mut Command) {
99        command.arg("-T0");
100    }
101}
102
103#[cfg(not(feature = "lt2023_2"))]
104impl ExclusiveOption for storage::TraitDepot {
105    fn inject_args(&self, command: &mut Command) {
106        command.arg("-T1");
107    }
108}
109
110#[cfg_attr(
111    feature = "lt2023_2",
112    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p] -i -n name file`"
113)]
114#[cfg_attr(
115    all(feature = "lt2024_2", not(feature = "lt2023_2")),
116    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`"
117)]
118#[cfg_attr(
119    not(feature = "lt2024_2"),
120    doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`\n\n`p4 attribute [-f -p [-T0 | -T1]] -I filename -n name file`"
121)]
122///
123/// The `S` type parameter tracks the value source: [`Standard`] for the
124/// default `-n`/`-v` set-or-clear form, [`FromStdin`] for the `-i` form,
125#[cfg_attr(
126    not(feature = "lt2024_2"),
127    doc = "[`FromFile`] for the `-I filename` form, and"
128)]
129/// and the `T` type parameter tracks the `-T0`/`-T1` trait storage location.
130#[derive(Debug, Clone, Default)]
131pub struct Attribute<S = Standard, T = Unselected> {
132    bin: PathBuf,
133
134    global_opts: GlobalOpts,
135
136    source: S,
137
138    storage: T,
139
140    on_submitted_files: bool,
141
142    propagating: bool,
143}
144
145impl<S: ExclusiveOption, T: ExclusiveOption> SubCommand for Attribute<S, T> {
146    fn name(&self) -> &str {
147        "attribute"
148    }
149
150    fn inject_local_args(&self, command: &mut Command) {
151        if self.on_submitted_files {
152            command.arg("-f");
153        }
154
155        if self.propagating {
156            command.arg("-p");
157        }
158
159        self.storage.inject_args(command);
160
161        self.source.inject_args(command);
162    }
163
164    fn global_opts(&self) -> Option<&GlobalOpts> {
165        Some(&self.global_opts)
166    }
167}
168
169impl Attribute<Standard, Unselected> {
170    /// Creates a new `p4 attribute` command.
171    ///
172    /// `bin` is the path to the Perforce command-line executable.
173    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
174        Self {
175            bin: bin.into(),
176            global_opts,
177            on_submitted_files: false,
178            propagating: false,
179            source: Standard::default(),
180            storage: Unselected,
181        }
182    }
183}
184
185impl<T: ExclusiveOption> ParameterizedSpawn for Attribute<Standard, T> {
186    type Input<'a> = &'a [&'a OsStr];
187    type Output<'a> = Child;
188    type Error = std::io::Error;
189
190    /// Spawns `p4 attribute` for the given files as a child process with
191    /// piped standard output and error streams; use the returned [`Child`]
192    /// handle to wait for it or interact with it.
193    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
194        self.setup_command(&self.bin)
195            .args(files)
196            .stdout(Stdio::piped())
197            .stderr(Stdio::piped())
198            .spawn()
199    }
200}
201
202impl<T: ExclusiveOption> Attribute<Standard, T> {
203    /// # Description
204    ///
205    /// -n name -v value
206    ///
207    /// Sets the attribute `name` to `value` on the given files.
208    #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
209    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
210        self.source
211            .values
212            .get_or_insert_with(Vec::new)
213            .push((name.into(), Some(value.into())));
214        self
215    }
216
217    /// # Description
218    ///
219    /// -n name
220    ///
221    /// Clears the attribute `name` on the given files by omitting the `-v`
222    /// option.
223    pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
224        self.source
225            .values
226            .get_or_insert_with(Vec::new)
227            .push((name.into(), None));
228        self
229    }
230
231    /// # Description
232    ///
233    /// -e
234    ///
235    /// Indicates that the value is specified in hex.
236    pub fn get_hex(&self) -> bool {
237        self.source.hex
238    }
239
240    /// # Description
241    ///
242    /// -e
243    ///
244    /// Indicates that the value is specified in hex.
245    pub fn set_hex(&mut self, v: bool) -> &mut Self {
246        self.source.hex = v;
247        self
248    }
249
250    /// # Description
251    ///
252    /// -e
253    ///
254    /// Indicates that the value is specified in hex.
255    pub fn hex(mut self, v: bool) -> Self {
256        self.source.hex = v;
257        self
258    }
259
260    /// # Description
261    ///
262    /// -i
263    ///
264    #[cfg_attr(
265        feature = "lt2024_2",
266        doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
267    )]
268    #[cfg_attr(
269        not(feature = "lt2024_2"),
270        doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option. This option supports both textual and binary content."
271    )]
272    ///
273    /// Transitions this command to the [`FromStdin`] state; any `-n`/`-v`
274    /// pairs set with [`Self::set`] or [`Self::clear`] are discarded, while
275    /// `-e` is preserved.
276    pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
277        Attribute {
278            bin: self.bin,
279            global_opts: self.global_opts,
280            on_submitted_files: self.on_submitted_files,
281            propagating: self.propagating,
282            source: FromStdin {
283                hex: self.source.hex,
284                name,
285            },
286            storage: self.storage,
287        }
288    }
289
290    /// # Description
291    ///
292    /// -I filename
293    ///
294    /// Read the attribute value from a file. The file can have textual or
295    /// binary content. Use this when the attribute data exceeds 250
296    /// megabytes, which might cause the command to fail with the `Rpc buffer
297    /// too big` error. The following are not allowed with this option:
298    ///
299    /// - Using the `-e` option to specify the value as hex.
300    /// - More than one file argument.
301    /// - Setting more than one trait value.
302    ///
303    /// To display attributes set with this option, use the `p4 print -T`
304    /// command instead of the `p4 fstat -Oa` command because `p4 print -T`
305    /// can handle larger non-encoded binary data.
306    ///
307    /// Transitions this command to the [`FromFile`] state; any `-n`/`-v`
308    /// pairs set with [`Self::set`] or [`Self::clear`], as well as `-e`, are
309    /// discarded.
310    #[cfg(not(feature = "lt2024_2"))]
311    pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
312        Attribute {
313            bin: self.bin,
314            global_opts: self.global_opts,
315            on_submitted_files: self.on_submitted_files,
316            propagating: self.propagating,
317            source: FromFile { name, file },
318            storage: self.storage,
319        }
320    }
321}
322
323impl<T: ExclusiveOption> Attribute<FromStdin, T> {
324    /// # Description
325    ///
326    /// -e
327    ///
328    /// Indicates that the value is specified in hex.
329    pub fn get_hex(&self) -> bool {
330        self.source.hex
331    }
332
333    /// # Description
334    ///
335    /// -e
336    ///
337    /// Indicates that the value is specified in hex.
338    pub fn set_hex(&mut self, v: bool) -> &mut Self {
339        self.source.hex = v;
340        self
341    }
342
343    /// # Description
344    ///
345    /// -e
346    ///
347    /// Indicates that the value is specified in hex.
348    pub fn hex(mut self, v: bool) -> Self {
349        self.source.hex = v;
350        self
351    }
352
353    /// # Description
354    ///
355    /// -n name
356    ///
357    /// The name of the attribute read from standard input.
358    pub fn get_name(&self) -> &str {
359        &self.source.name
360    }
361
362    /// # Description
363    ///
364    /// -n name
365    ///
366    /// The name of the attribute read from standard input.
367    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
368        self.source.name = v.into();
369        self
370    }
371
372    /// # Description
373    ///
374    /// -n name
375    ///
376    /// The name of the attribute read from standard input.
377    pub fn name(mut self, v: impl Into<String>) -> Self {
378        self.source.name = v.into();
379        self
380    }
381
382    /// Runs `p4 attribute -i` for a single file, writing `value` to the
383    /// child's standard input, and captures the command's output.
384    ///
385    /// Unlike [`ParameterizedSpawn::spawn_with`], this method handles writing
386    /// the attribute value to the child's standard input itself, so it is the
387    /// most convenient way to submit values through standard input.
388    ///
389    /// Only one file argument is allowed in the [`FromStdin`] state.
390    pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
391    where
392        S: AsRef<OsStr>,
393        V: AsRef<[u8]>,
394    {
395        let mut child = self
396            .setup_command(&self.bin)
397            .arg(file)
398            .stdin(Stdio::piped())
399            .stdout(Stdio::piped())
400            .stderr(Stdio::piped())
401            .spawn()?;
402
403        if let Some(ref mut stdin) = child.stdin {
404            stdin.write_all(value.as_ref())?;
405        }
406        drop(child.stdin.take());
407
408        child.wait_with_output()
409    }
410}
411
412impl<T: ExclusiveOption> ParameterizedSpawn for Attribute<FromStdin, T> {
413    type Input<'a> = (&'a OsStr, Stdio);
414    type Output<'a> = Child;
415    type Error = std::io::Error;
416
417    /// Spawns `p4 attribute -i` for a single file as a child process with the
418    /// given standard input configuration and piped standard output and error
419    /// streams; use the returned [`Child`] handle to wait for it or interact
420    /// with it.
421    ///
422    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
423    /// write the attribute value yourself, then drop it before waiting on
424    /// the child. Only one file argument is allowed in the [`FromStdin`]
425    /// state.
426    fn spawn_with<'a>(&mut self, input: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
427        self.setup_command(&self.bin)
428            .arg(input.0)
429            .stdin(input.1)
430            .stdout(Stdio::piped())
431            .stderr(Stdio::piped())
432            .spawn()
433    }
434}
435
436#[cfg(not(feature = "lt2024_2"))]
437impl<T: ExclusiveOption> Attribute<FromFile, T> {
438    /// # Description
439    ///
440    /// -n name
441    ///
442    /// The name of the attribute read from the file.
443    pub fn get_name(&self) -> &str {
444        &self.source.name
445    }
446
447    /// # Description
448    ///
449    /// -n name
450    ///
451    /// The name of the attribute read from the file.
452    pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
453        self.source.name = v.into();
454        self
455    }
456
457    /// # Description
458    ///
459    /// -n name
460    ///
461    /// The name of the attribute read from the file.
462    pub fn name(mut self, v: impl Into<String>) -> Self {
463        self.source.name = v.into();
464        self
465    }
466
467    /// # Description
468    ///
469    /// -I filename
470    ///
471    /// The file the attribute value is read from.
472    pub fn get_file(&self) -> &Path {
473        &self.source.file
474    }
475}
476
477#[cfg(not(feature = "lt2024_2"))]
478impl<T: ExclusiveOption> ParameterizedSpawn for Attribute<FromFile, T> {
479    type Input<'a> = &'a OsStr;
480    type Output<'a> = Child;
481    type Error = std::io::Error;
482
483    /// Spawns `p4 attribute -I` for a single file as a child process with
484    /// piped standard output and error streams; use the returned [`Child`]
485    /// handle to wait for it or interact with it.
486    ///
487    /// Only one file argument is allowed in the [`FromFile`] state.
488    fn spawn_with<'a>(&mut self, file: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
489        self.setup_command(&self.bin)
490            .arg(file)
491            .stdout(Stdio::piped())
492            .stderr(Stdio::piped())
493            .spawn()
494    }
495}
496
497impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
498    /// # Description
499    ///
500    /// g-opts
501    ///
502    #[cfg_attr(
503        feature = "lt2014_2",
504        doc = "See the [Global Options](GlobalOpts) section."
505    )]
506    #[cfg_attr(
507        all(feature = "lt2015_1", not(feature = "lt2014_2")),
508        doc = "See the [“Global Options”](GlobalOpts) section."
509    )]
510    #[cfg_attr(
511        all(feature = "lt2017_1", not(feature = "lt2015_1")),
512        doc = "See [“Global Options”](GlobalOpts)."
513    )]
514    #[cfg_attr(
515        all(feature = "lt2018_2", not(feature = "lt2017_1")),
516        doc = "See [Global Options](GlobalOpts)."
517    )]
518    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
519    pub fn get_global_opts(&self) -> &GlobalOpts {
520        &self.global_opts
521    }
522
523    /// # Description
524    ///
525    /// g-opts
526    ///
527    #[cfg_attr(
528        feature = "lt2014_2",
529        doc = "See the [Global Options](GlobalOpts) section."
530    )]
531    #[cfg_attr(
532        all(feature = "lt2015_1", not(feature = "lt2014_2")),
533        doc = "See the [“Global Options”](GlobalOpts) section."
534    )]
535    #[cfg_attr(
536        all(feature = "lt2017_1", not(feature = "lt2015_1")),
537        doc = "See [“Global Options”](GlobalOpts)."
538    )]
539    #[cfg_attr(
540        all(feature = "lt2018_2", not(feature = "lt2017_1")),
541        doc = "See [Global Options](GlobalOpts)."
542    )]
543    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
544    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
545        self.global_opts = v;
546        self
547    }
548
549    /// # Description
550    ///
551    /// g-opts
552    ///
553    #[cfg_attr(
554        feature = "lt2014_2",
555        doc = "See the [Global Options](GlobalOpts) section."
556    )]
557    #[cfg_attr(
558        all(feature = "lt2015_1", not(feature = "lt2014_2")),
559        doc = "See the [“Global Options”](GlobalOpts) section."
560    )]
561    #[cfg_attr(
562        all(feature = "lt2017_1", not(feature = "lt2015_1")),
563        doc = "See [“Global Options”](GlobalOpts)."
564    )]
565    #[cfg_attr(
566        all(feature = "lt2018_2", not(feature = "lt2017_1")),
567        doc = "See [Global Options](GlobalOpts)."
568    )]
569    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
570    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
571        self.global_opts = v;
572        self
573    }
574
575    /// # Description
576    ///
577    /// -f
578    ///
579    /// Set the attribute on submitted files. If a propagating trait is set
580    /// on a submitted file, a revision specifier cannot be used, and the
581    /// file must not be currently open in any workspace.
582    pub fn get_on_submitted_files(&self) -> bool {
583        self.on_submitted_files
584    }
585
586    /// # Description
587    ///
588    /// -f
589    ///
590    /// Set the attribute on submitted files. If a propagating trait is set
591    /// on a submitted file, a revision specifier cannot be used, and the
592    /// file must not be currently open in any workspace.
593    pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
594        self.on_submitted_files = v;
595        self
596    }
597
598    /// # Description
599    ///
600    /// -f
601    ///
602    /// Set the attribute on submitted files. If a propagating trait is set
603    /// on a submitted file, a revision specifier cannot be used, and the
604    /// file must not be currently open in any workspace.
605    pub fn on_submitted_files(mut self, v: bool) -> Self {
606        self.on_submitted_files = v;
607        self
608    }
609
610    /// # Description
611    ///
612    /// -p
613    ///
614    #[cfg_attr(
615        feature = "lt2021_2",
616        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
617    )]
618    #[cfg_attr(
619        not(feature = "lt2021_2"),
620        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
621    )]
622    pub fn get_propagating(&self) -> bool {
623        self.propagating
624    }
625
626    /// # Description
627    ///
628    /// -p
629    ///
630    #[cfg_attr(
631        feature = "lt2021_2",
632        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
633    )]
634    #[cfg_attr(
635        not(feature = "lt2021_2"),
636        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
637    )]
638    pub fn set_propagating(&mut self, v: bool) -> &mut Self {
639        self.propagating = v;
640        self
641    }
642
643    /// # Description
644    ///
645    /// -p
646    ///
647    #[cfg_attr(
648        feature = "lt2021_2",
649        doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
650    )]
651    #[cfg_attr(
652        not(feature = "lt2021_2"),
653        doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
654    )]
655    pub fn propagating(mut self, v: bool) -> Self {
656        self.propagating = v;
657        self
658    }
659}
660
661#[cfg(not(feature = "lt2023_2"))]
662impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
663    /// # Description
664    ///
665    /// -T0
666    ///
667    /// Causes the value to be stored in the `db.traits` table, which is the
668    /// implicit default.
669    pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
670        Attribute {
671            bin: self.bin,
672            global_opts: self.global_opts,
673            on_submitted_files: self.on_submitted_files,
674            propagating: self.propagating,
675            source: self.source,
676            storage: storage::DatabaseTraits,
677        }
678    }
679
680    /// # Description
681    ///
682    /// -T1
683    ///
684    /// Causes the value to be stored in the `trait` depot instead of the
685    /// `db.traits` table, even if the size of the attribute value is less
686    /// than the size specified by the `trait.storagedepot.min` configurable.
687    /// However, if `trait.storagedepot.min` is unset or set to `0`, the
688    /// attribute value is stored in the `db.traits` table.
689    pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
690        Attribute {
691            bin: self.bin,
692            global_opts: self.global_opts,
693            on_submitted_files: self.on_submitted_files,
694            propagating: self.propagating,
695            source: self.source,
696            storage: storage::TraitDepot,
697        }
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use crate::cmd::args_of;
705
706    /// Dry-run checks of the assembled `p4 attribute` command line; no
707    /// process is spawned.
708    #[test]
709    fn without_options() {
710        let attr = Attribute::new("p4", GlobalOpts::new());
711
712        assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
713    }
714
715    #[test]
716    fn set_attribute_with_value() {
717        let mut attr = Attribute::new("p4", GlobalOpts::new());
718        attr.set("status", "approved");
719
720        assert_eq!(
721            args_of(&attr.setup_command("p4")),
722            ["attribute", "-n", "status", "-v", "approved"]
723        );
724    }
725
726    #[test]
727    fn clear_attribute_omits_value() {
728        let mut attr = Attribute::new("p4", GlobalOpts::new());
729        attr.clear("status");
730
731        assert_eq!(
732            args_of(&attr.setup_command("p4")),
733            ["attribute", "-n", "status"]
734        );
735    }
736
737    #[test]
738    fn multiple_set_and_clear_pairs_keep_order() {
739        let mut attr = Attribute::new("p4", GlobalOpts::new());
740        attr.set("color", "red")
741            .clear("status")
742            .set("owner", "alice");
743
744        assert_eq!(
745            args_of(&attr.setup_command("p4")),
746            [
747                "attribute",
748                "-n",
749                "color",
750                "-v",
751                "red",
752                "-n",
753                "status",
754                "-n",
755                "owner",
756                "-v",
757                "alice"
758            ]
759        );
760    }
761
762    #[test]
763    fn hex_submitted_propagating() {
764        let mut attr = Attribute::new("p4", GlobalOpts::new());
765        attr.set_hex(true)
766            .set_on_submitted_files(true)
767            .set_propagating(true)
768            .set("thumb", "deadbeef");
769
770        assert_eq!(
771            args_of(&attr.setup_command("p4")),
772            [
773                "attribute",
774                "-f",
775                "-p",
776                "-e",
777                "-n",
778                "thumb",
779                "-v",
780                "deadbeef"
781            ]
782        );
783    }
784
785    #[test]
786    fn read_from_stdin_state() {
787        let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
788
789        assert_eq!(attr.get_name(), "thumb");
790        assert_eq!(
791            args_of(&attr.setup_command("p4")),
792            ["attribute", "-i", "-n", "thumb"]
793        );
794    }
795
796    #[test]
797    fn stdin_name_can_be_replaced() {
798        let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
799        attr.set_name("thumb");
800
801        assert_eq!(attr.get_name(), "thumb");
802
803        let attr = attr.name("icon");
804        assert_eq!(attr.get_name(), "icon");
805        assert_eq!(
806            args_of(&attr.setup_command("p4")),
807            ["attribute", "-i", "-n", "icon"]
808        );
809    }
810
811    #[test]
812    fn stdin_preserves_flags_and_hex() {
813        let mut attr = Attribute::new("p4", GlobalOpts::new());
814        attr.set_hex(true)
815            .set_on_submitted_files(true)
816            .set_propagating(true)
817            .set("discarded", "value");
818
819        let attr = attr.read_from_stdin("thumb".to_string());
820
821        assert!(attr.get_hex());
822        assert_eq!(
823            args_of(&attr.setup_command("p4")),
824            ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
825        );
826    }
827
828    #[cfg(not(feature = "lt2023_2"))]
829    #[test]
830    fn store_in_database_traits() {
831        let mut attr = Attribute::new("p4", GlobalOpts::new());
832        attr.set("thumb", "data");
833
834        let attr = attr.store_in_database_traits();
835
836        assert_eq!(
837            args_of(&attr.setup_command("p4")),
838            ["attribute", "-T0", "-n", "thumb", "-v", "data"]
839        );
840    }
841
842    #[cfg(not(feature = "lt2023_2"))]
843    #[test]
844    fn store_in_trait_depot() {
845        let mut attr = Attribute::new("p4", GlobalOpts::new());
846        attr.set("thumb", "data");
847
848        let attr = attr.store_in_trait_depot();
849
850        assert_eq!(
851            args_of(&attr.setup_command("p4")),
852            ["attribute", "-T1", "-n", "thumb", "-v", "data"]
853        );
854    }
855
856    #[cfg(not(feature = "lt2023_2"))]
857    #[test]
858    fn stdin_with_trait_depot() {
859        let attr = Attribute::new("p4", GlobalOpts::new())
860            .read_from_stdin("thumb".to_string())
861            .store_in_trait_depot();
862
863        assert_eq!(
864            args_of(&attr.setup_command("p4")),
865            ["attribute", "-T1", "-i", "-n", "thumb"]
866        );
867    }
868
869    #[cfg(not(feature = "lt2024_2"))]
870    #[test]
871    fn read_from_file_state() {
872        let attr = Attribute::new("p4", GlobalOpts::new())
873            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
874
875        assert_eq!(attr.get_name(), "thumb");
876        assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
877        assert_eq!(
878            args_of(&attr.setup_command("p4")),
879            ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
880        );
881    }
882
883    #[cfg(not(feature = "lt2024_2"))]
884    #[test]
885    fn all_modern_options() {
886        let mut attr = Attribute::new("p4", GlobalOpts::new());
887        attr.set_on_submitted_files(true).set_propagating(true);
888
889        let attr = attr
890            .store_in_trait_depot()
891            .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
892
893        assert_eq!(
894            args_of(&attr.setup_command("p4")),
895            [
896                "attribute",
897                "-f",
898                "-p",
899                "-T1",
900                "-I",
901                "/tmp/data.bin",
902                "-n",
903                "thumb"
904            ]
905        );
906    }
907
908    #[test]
909    fn set_style_with_global_opts() {
910        let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
911        attr.set("status", "approved");
912
913        assert_eq!(
914            args_of(&attr.setup_command("p4")),
915            [
916                "-p",
917                "localhost:1666",
918                "attribute",
919                "-n",
920                "status",
921                "-v",
922                "approved"
923            ]
924        );
925    }
926}